mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-12 19:31:55 +00:00
Merge branch 'development' into feat/react-router
This commit is contained in:
commit
3cabd4a51e
58 changed files with 603 additions and 344 deletions
65
.github/workflows/auto_assign.yml
vendored
Normal file
65
.github/workflows/auto_assign.yml
vendored
Normal file
|
|
@ -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
|
||||
});
|
||||
}
|
||||
46
package-lock.json
generated
46
package-lock.json
generated
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
</div>
|
||||
<Button className={styles['close-button-container']} title={'Close'} tabIndex={-1} onClick={closeButtonOnClick}>
|
||||
<Button className={styles['close-button-container']} title={t('BUTTON_CLOSE')} tabIndex={-1} onClick={closeButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const routesRegexp = {
|
|||
urlParamsNames: []
|
||||
},
|
||||
board: {
|
||||
regexp: /^\/?$/,
|
||||
regexp: /^\/?(?:board)?$/,
|
||||
urlParamsNames: []
|
||||
},
|
||||
discover: {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<Icon className={styles['icon']} name={'addons'} />
|
||||
), []);
|
||||
|
|
@ -24,7 +26,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
|
|||
<span className={styles['name']}>{typeof name === 'string' && name.length > 0 ? name : id}</span>
|
||||
{
|
||||
typeof version === 'string' && version.length > 0 ?
|
||||
<span className={styles['version']}>v. {version}</span>
|
||||
<span className={styles['version']}>{t('ADDON_VERSION_SHORT', {version})}</span>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
|
@ -41,7 +43,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
|
|||
{
|
||||
typeof transportUrl === 'string' && transportUrl.length > 0 ?
|
||||
<div className={styles['section-container']}>
|
||||
<span className={styles['section-header']}>URL: </span>
|
||||
<span className={styles['section-header']}>{`${t('URL')}:`}</span>
|
||||
<span className={classnames(styles['section-label'], styles['transport-url-label'])}>{transportUrl}</span>
|
||||
</div>
|
||||
:
|
||||
|
|
@ -50,7 +52,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
|
|||
{
|
||||
Array.isArray(types) && types.length > 0 ?
|
||||
<div className={styles['section-container']}>
|
||||
<span className={styles['section-header']}>Supported types: </span>
|
||||
<span className={styles['section-header']}>{`${t('ADDON_SUPPORTED_TYPES')}:`} </span>
|
||||
<span className={styles['section-label']}>
|
||||
{
|
||||
types.length === 1 ?
|
||||
|
|
@ -66,7 +68,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
|
|||
{
|
||||
!official ?
|
||||
<div className={styles['section-container']}>
|
||||
<div className={classnames(styles['section-label'], styles['disclaimer-label'])}>Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.</div>
|
||||
<div className={classnames(styles['section-label'], styles['disclaimer-label'])}>{t('ADDON_DISCLAIMER')}</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ModalDialog className={styles['addon-details-modal-container']} title={'Stremio addon'} buttons={modalButtons} background={modalBackground} onCloseRequest={onCloseRequest}>
|
||||
<ModalDialog className={styles['addon-details-modal-container']} title={t('STREMIO_COMMUNITY_ADDON')} buttons={modalButtons} background={modalBackground} onCloseRequest={onCloseRequest}>
|
||||
{
|
||||
addonDetails.selected === null ?
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
Loading addon manifest
|
||||
{t('ADDON_LOADING_MANIFEST')}
|
||||
</div>
|
||||
:
|
||||
addonDetails.remoteAddon === null || addonDetails.remoteAddon.content.type === 'Loading' ?
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
Loading addon manifest from {addonDetails.selected.transportUrl}
|
||||
{t('ADDON_LOADING_MANIFEST_FROM', { origin: addonDetails.selected.transportUrl})}
|
||||
</div>
|
||||
:
|
||||
addonDetails.remoteAddon.content.type === 'Err' && addonDetails.localAddon === null ?
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
Failed to get addon manifest from {addonDetails.selected.transportUrl}
|
||||
{t('ADDON_LOADING_MANIFEST_FAILED', {origin: addonDetails.selected.transportUrl})}
|
||||
<div>{addonDetails.remoteAddon.content.content.message}</div>
|
||||
</div>
|
||||
:
|
||||
|
|
@ -174,17 +176,18 @@ AddonDetailsModal.propTypes = {
|
|||
onCloseRequest: PropTypes.func
|
||||
};
|
||||
|
||||
const AddonDetailsModalFallback = ({ onCloseRequest }) => (
|
||||
<ModalDialog
|
||||
const AddonDetailsModalFallback = ({ onCloseRequest }) => {
|
||||
const { t } = useTranslation();
|
||||
return <ModalDialog
|
||||
className={styles['addon-details-modal-container']}
|
||||
title={'Stremio addon'}
|
||||
title={t('STREMIO_COMMUNITY_ADDON')}
|
||||
onCloseRequest={onCloseRequest}
|
||||
>
|
||||
<div className={styles['addon-details-message-container']}>
|
||||
Loading addon manifest
|
||||
{t('ADDON_LOADING_MANIFEST')}
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
</ModalDialog>;
|
||||
};
|
||||
|
||||
AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
.link {
|
||||
font-size: 0.9rem;
|
||||
color: var(--primary-accent-color);
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ const Checkbox = React.forwardRef<HTMLInputElement, Props>(({ name, disabled, cl
|
|||
</div>
|
||||
<div>
|
||||
<span>{label}</span>
|
||||
{' '}
|
||||
{
|
||||
href && link ?
|
||||
<Button className={styles['link']} href={href} target={'_blank'} tabIndex={-1}>
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
<ModalDialog title={'Choose a color:'} buttons={modalButtons} onCloseRequest={closeModal} onClick={modalDialogOnClick}>
|
||||
<ModalDialog title={t('CHOOSE_COLOR')} buttons={modalButtons} onCloseRequest={closeModal} onClick={modalDialogOnClick}>
|
||||
<ColorPicker className={styles['color-picker-container']} value={tempValue} onInput={colorPickerOnInput} />
|
||||
</ModalDialog>
|
||||
:
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<div className={styles['logo-placeholder']}>{name}</div>
|
||||
), [name]);
|
||||
return (
|
||||
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })}>
|
||||
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
|
||||
{
|
||||
typeof background === 'string' && background.length > 0 ?
|
||||
<div className={styles['background-image-layer']}>
|
||||
|
|
@ -261,7 +261,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
MetaPreview.Placeholder = MetaPreviewPlaceholder;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<Modal ref={modalContainerRef} {...props} className={classnames(className, styles['modal-container'])} onMouseDown={onModalContainerMouseDown}>
|
||||
<div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}>
|
||||
<div className={styles['modal-dialog-background']} style={{backgroundImage: `url('${background}')`}} />
|
||||
<Button className={styles['close-button-container']} title={'Close'} onClick={closeButtonOnClick}>
|
||||
<Button className={styles['close-button-container']} title={t('BUTTON_CLOSE')} onClick={closeButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
<div className={styles['modal-dialog-content']}>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
))
|
||||
:
|
||||
<div className={styles['no-options-container']}>
|
||||
<div className={styles['label']}>No options available</div>
|
||||
<div className={styles['label']}>{t('NO_OPTIONS')}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
@parent-height: 10rem;
|
||||
@parent-height: 12rem;
|
||||
|
||||
.dropdown {
|
||||
background: var(--modal-background-color);
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<Option
|
||||
key={option.id}
|
||||
key={option.value}
|
||||
ref={handleSetOptionRef(option.value)}
|
||||
option={option}
|
||||
onSelect={onSelect}
|
||||
selectedOption={selectedOption}
|
||||
selectedValue={value}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement, Props>(({ option, selectedOption, onSelect }, ref) => {
|
||||
// consider using option.id === selectedOption?.id instead
|
||||
const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
|
||||
const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedValue, onSelect }, ref) => {
|
||||
const selected = useMemo(() => option?.value === selectedValue, [option, selectedValue]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect(option.value);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<number>(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 (
|
||||
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
|
||||
<div className={classNames(styles['multiselect-menu'], { [styles['active']]: menuOpen }, className)} ref={multiselectMenuRef}>
|
||||
<Button
|
||||
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
|
||||
onClick={toggleMenu}
|
||||
|
|
@ -35,7 +37,13 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
|
|||
aria-haspopup='listbox'
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
{title}
|
||||
<div className={styles['label']}>
|
||||
{
|
||||
typeof title === 'function'
|
||||
? title()
|
||||
: title ?? selectedOption?.label
|
||||
}
|
||||
</div>
|
||||
<Icon name={'caret-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
|
||||
</Button>
|
||||
{
|
||||
|
|
@ -46,7 +54,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
|
|||
options={options}
|
||||
onSelect={onOptionSelect}
|
||||
menuOpen={menuOpen}
|
||||
selectedOption={selectedOption}
|
||||
value={value}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const SharePrompt = ({ className, url }) => {
|
|||
onClick={selectInputContent}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<Button className={styles['copy-button']} title={'Copy to clipboard'} onClick={copyToClipboard}>
|
||||
<Button className={styles['copy-button']} title={t('CTX_COPY_TO_CLIPBOARD')} onClick={copyToClipboard}>
|
||||
<Icon className={styles['icon']} name={'link'} />
|
||||
<div className={styles['label']}>{ t('COPY') }</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -110,12 +110,12 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
|
|||
{
|
||||
released instanceof Date && !isNaN(released.getTime()) ?
|
||||
<div className={styles['released-container']}>
|
||||
{released.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
{released.toLocaleString(profile.settings.interfaceLanguage, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
:
|
||||
scheduled ?
|
||||
<div className={styles['released-container']} title={'To be announced'}>
|
||||
TBA
|
||||
<div className={styles['released-container']} title={t('TBA')}>
|
||||
{t('TBA')}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
@ -124,7 +124,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
|
|||
{
|
||||
upcoming && !watched ?
|
||||
<div className={styles['upcoming-container']}>
|
||||
<div className={styles['flag-label']}>Upcoming</div>
|
||||
<div className={styles['flag-label']}>{t('UPCOMING')}</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
@ -133,7 +133,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
|
|||
watched ?
|
||||
<div className={styles['watched-container']}>
|
||||
<Icon className={styles['flag-icon']} name={'eye'} />
|
||||
<div className={styles['flag-label']}>Watched</div>
|
||||
<div className={styles['flag-label']}>{t('CTX_WATCHED')}</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
@ -148,10 +148,10 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
|
|||
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||
return (
|
||||
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
|
||||
<Button className={styles['context-menu-option-container']} title={'Watch'}>
|
||||
<Button className={styles['context-menu-option-container']} title={t('CTX_WATCH')}>
|
||||
<div className={styles['context-menu-option-label']}>{t('CTX_WATCH')}</div>
|
||||
</Button>
|
||||
<Button className={styles['context-menu-option-container']} title={watched ? 'Mark as non-watched' : 'Mark as watched'} onClick={toggleWatchedOnClick}>
|
||||
<Button className={styles['context-menu-option-container']} title={watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')} onClick={toggleWatchedOnClick}>
|
||||
<div className={styles['context-menu-option-label']}>{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}</div>
|
||||
</Button>
|
||||
<Button className={styles['context-menu-option-container']} title={seasonWatched ? t('CTX_UNMARK_REST') : t('CTX_MARK_REST')} onClick={toggleWatchedSeasonOnClick}>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ const Addon = ({ className, id, name, version, logo, description, types, behavio
|
|||
</div>
|
||||
{
|
||||
typeof version === 'string' && version.length > 0 ?
|
||||
<div className={styles['version-container']} title={`v.${version}`}>v.{version}</div>
|
||||
<div className={styles['version-container']} title={t('ADDON_VERSION_SHORT', {version})}>{t('ADDON_VERSION_SHORT', {version})}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<div className={styles['addons-content']}>
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
{selectInputs.map((selectInput, index) => (
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
{...selectInput}
|
||||
key={index}
|
||||
className={styles['select-input-container']}
|
||||
|
|
@ -127,7 +127,7 @@ const Addons = () => {
|
|||
value={search}
|
||||
onChange={searchInputOnChange}
|
||||
/>
|
||||
<Button className={styles['filter-button']} title={'All filters'} onClick={openFiltersModal}>
|
||||
<Button className={styles['filter-button']} title={t('ALL_FILTERS')} onClick={openFiltersModal}>
|
||||
<Icon className={styles['filter-icon']} name={'filters'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -135,12 +135,12 @@ const Addons = () => {
|
|||
installedAddons.selected !== null ?
|
||||
installedAddons.selectable.types.length === 0 ?
|
||||
<div className={styles['message-container']}>
|
||||
No addons ware installed!
|
||||
{t('NO_ADDONS')}
|
||||
</div>
|
||||
:
|
||||
installedAddons.catalog.length === 0 ?
|
||||
<div className={styles['message-container']}>
|
||||
No addons ware installed for that type!
|
||||
{t('NO_ADDONS_FOR_TYPE')}
|
||||
</div>
|
||||
:
|
||||
<div className={styles['addons-list-container']}>
|
||||
|
|
@ -219,9 +219,9 @@ const Addons = () => {
|
|||
</div>
|
||||
{
|
||||
filtersModalOpen ?
|
||||
<ModalDialog title={'Addons filters'} className={styles['filters-modal']} onCloseRequest={closeFiltersModal}>
|
||||
<ModalDialog title={t('ADDONS_FILTERS')} className={styles['filters-modal']} onCloseRequest={closeFiltersModal}>
|
||||
{selectInputs.map((selectInput, index) => (
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
{...selectInput}
|
||||
key={index}
|
||||
className={styles['select-input-container']}
|
||||
|
|
@ -268,7 +268,7 @@ const Addons = () => {
|
|||
<span className={styles['name']}>{typeof sharedAddon.manifest.name === 'string' && sharedAddon.manifest.name.length > 0 ? sharedAddon.manifest.name : sharedAddon.manifest.id}</span>
|
||||
{
|
||||
typeof sharedAddon.manifest.version === 'string' && sharedAddon.manifest.version.length > 0 ?
|
||||
<span className={styles['version']}>v. {sharedAddon.manifest.version}</span>
|
||||
<span className={styles['version']}>{t('ADDON_VERSION_SHORT', { version: sharedAddon.manifest.version })}</span>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
}
|
||||
|
||||
.select-input-container {
|
||||
background-color: var(--overlay-color);
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 15rem;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
<div className={styles['placeholder']}>
|
||||
No new episodes for this day
|
||||
{t('CALENDAR_NO_NEW_EPISODES')}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<div className={styles['discover-content']}>
|
||||
<div className={styles['catalog-container']}>
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
|
||||
<Multiselect
|
||||
{selectInputs.map(({ title, options, value, onSelect }, index) => (
|
||||
<MultiselectMenu
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selected={selected}
|
||||
renderLabelText={renderLabelText}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
<div className={styles['filter-container']}>
|
||||
<Button className={styles['filter-button']} title={'All filters'} onClick={openInputsModal}>
|
||||
<Button className={styles['filter-button']} title={t('ALL_FILTERS')} onClick={openInputsModal}>
|
||||
<Icon className={styles['filter-icon']} name={'filters'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -119,9 +124,9 @@ const Discover = () => {
|
|||
{
|
||||
discover.catalog !== null && !discover.catalog.installed ?
|
||||
<div className={styles['missing-addon-warning-container']}>
|
||||
<div className={styles['warning-label']}>Addon is not installed. Install now?</div>
|
||||
<Button className={styles['install-button']} title={'Install addon'} onClick={openAddonModal}>
|
||||
<div className={styles['label']}>Install</div>
|
||||
<div className={styles['warning-label']}>{t('ERR_ADDON_NOT_INSTALLED')}</div>
|
||||
<Button className={styles['install-button']} title={t('INSTALL_ADDON')} onClick={openAddonModal}>
|
||||
<div className={styles['label']}>{t('ADDON_INSTALL')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
|
|
@ -132,7 +137,7 @@ const Discover = () => {
|
|||
<DelayedRenderer delay={500}>
|
||||
<div className={styles['message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No catalog selected!</div>
|
||||
<div className={styles['message-label']}>{t('NO_CATALOG_SELECTED')}</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
|
|
@ -178,6 +183,7 @@ const Discover = () => {
|
|||
<MetaPreview
|
||||
className={styles['meta-preview-container']}
|
||||
compact={true}
|
||||
ref={metaPreviewRef}
|
||||
name={selectedMetaItem.name}
|
||||
logo={selectedMetaItem.logo}
|
||||
background={selectedMetaItem.poster}
|
||||
|
|
@ -200,15 +206,14 @@ const Discover = () => {
|
|||
</div>
|
||||
{
|
||||
inputsModalOpen ?
|
||||
<ModalDialog title={'Catalog filters'} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
|
||||
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
|
||||
<Multiselect
|
||||
<ModalDialog title={t('CATALOG_FILTERS')} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
|
||||
{selectInputs.map(({ title, options, value, onSelect }, index) => (
|
||||
<MultiselectMenu
|
||||
key={index}
|
||||
className={styles['select-input']}
|
||||
title={title}
|
||||
options={options}
|
||||
selected={selected}
|
||||
renderLabelText={renderLabelText}
|
||||
value={value}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
|
||||
.select-input {
|
||||
flex: 0 1 15rem;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 1.5rem;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -302,10 +302,10 @@ const Intro = () => {
|
|||
<Image className={styles['logo']} src={require('/images/logo.png')} alt={' '} />
|
||||
</div>
|
||||
<div className={styles['title-container']}>
|
||||
Freedom to Stream
|
||||
{t('WEBSITE_SLOGAN_NEW_NEW')}
|
||||
</div>
|
||||
<div className={styles['slogan-container']}>
|
||||
All the Video Content You Enjoy in One Place
|
||||
{t('WEBSITE_SLOGAN_ALL')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['content-container']}>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
<Checkbox
|
||||
ref={termsRef}
|
||||
label={'I have read and agree with the Stremio'}
|
||||
link={'Terms and conditions'}
|
||||
label={t('READ_AND_AGREE')}
|
||||
link={t('TOS')}
|
||||
href={'https://www.stremio.com/tos'}
|
||||
checked={state.termsAccepted}
|
||||
onChange={toggleTermsAccepted}
|
||||
/>
|
||||
<Checkbox
|
||||
ref={privacyPolicyRef}
|
||||
label={'I have read and agree with the Stremio'}
|
||||
link={'Privacy Policy'}
|
||||
label={t('READ_AND_AGREE')}
|
||||
link={t('PRIVACY_POLICY')}
|
||||
href={'https://www.stremio.com/privacy'}
|
||||
checked={state.privacyPolicyAccepted}
|
||||
onChange={togglePrivacyPolicyAccepted}
|
||||
/>
|
||||
<Checkbox
|
||||
ref={marketingRef}
|
||||
label={'I agree to receive marketing communications from Stremio'}
|
||||
label={t('MARKETING_AGREE')}
|
||||
checked={state.marketingAccepted}
|
||||
onChange={toggleMarketingAccepted}
|
||||
/>
|
||||
</React.Fragment>
|
||||
:
|
||||
<div className={styles['forgot-password-link-container']}>
|
||||
<Button className={styles['forgot-password-link']} onClick={openPasswordRestModal}>Forgot password?</Button>
|
||||
<Button className={styles['forgot-password-link']} onClick={openPasswordRestModal}>{t('FORGOT_PASSWORD')}</Button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
|
|
@ -375,22 +375,22 @@ const Intro = () => {
|
|||
null
|
||||
}
|
||||
<Button className={classnames(styles['form-button'], styles['submit-button'])} onClick={state.form === SIGNUP_FORM ? signup : loginWithEmail}>
|
||||
<div className={styles['label']}>{state.form === SIGNUP_FORM ? 'Sign up' : 'Log in'}</div>
|
||||
<div className={styles['label']}>{state.form === SIGNUP_FORM ? t('SIGN_UP') : t('LOG_IN')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles['options-container']}>
|
||||
<Button className={classnames(styles['form-button'], styles['facebook-button'])} onClick={loginWithFacebook}>
|
||||
<Icon className={styles['icon']} name={'facebook'} />
|
||||
<div className={styles['label']}>Continue with Facebook</div>
|
||||
<div className={styles['label']}>{t('FB_LOGIN')}</div>
|
||||
</Button>
|
||||
<Button className={classnames(styles['form-button'], styles['apple-button'])} onClick={loginWithApple}>
|
||||
<Icon className={styles['icon']} name={'macos'} />
|
||||
<div className={styles['label']}>Continue with Apple</div>
|
||||
<div className={styles['label']}>{t('APPLE_LOGIN')}</div>
|
||||
</Button>
|
||||
{
|
||||
state.form === SIGNUP_FORM ?
|
||||
<Button className={classnames(styles['form-button'], styles['login-form-button'])} onClick={switchFormOnClick}>
|
||||
<div className={styles['label']}>LOG IN</div>
|
||||
<div className={classnames(styles['label'], styles['uppercase'])}>{t('LOG_IN')}</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
|
|
@ -398,7 +398,7 @@ const Intro = () => {
|
|||
{
|
||||
state.form === LOGIN_FORM ?
|
||||
<Button className={classnames(styles['form-button'], styles['signup-form-button'])} onClick={switchFormOnClick}>
|
||||
<div className={styles['label']}>SIGN UP WITH EMAIL</div>
|
||||
<div className={classnames(styles['label'], styles['uppercase'])}>{t('SIGN_UP_EMAIL')}</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
|
|
@ -406,7 +406,7 @@ const Intro = () => {
|
|||
{
|
||||
state.form === SIGNUP_FORM ?
|
||||
<Button className={classnames(styles['form-button'], styles['guest-login-button'])} onClick={loginAsGuest}>
|
||||
<div className={styles['label']}>GUEST LOGIN</div>
|
||||
<div className={classnames(styles['label'], styles['uppercase'])}>{t('GUEST_LOGIN')}</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
|
|
@ -424,7 +424,7 @@ const Intro = () => {
|
|||
<Modal className={styles['loading-modal-container']}>
|
||||
<div className={styles['loader-container']}>
|
||||
<Icon className={styles['icon']} name={'person'} />
|
||||
<div className={styles['label']}>Authenticating...</div>
|
||||
<div className={styles['label']}>{t('AUTHENTICATING')}</div>
|
||||
<Button className={styles['button']} onClick={cancelLoginWithFacebook && cancelLoginWithApple}>
|
||||
{t('BUTTON_CANCEL')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ModalDialog className={styles['password-reset-modal-container']} title={'Password reset'} buttons={passwordResetModalButtons} onCloseRequest={onCloseRequest}>
|
||||
<ModalDialog className={styles['password-reset-modal-container']} title={t('PASSWORD_RESET')} buttons={passwordResetModalButtons} onCloseRequest={onCloseRequest}>
|
||||
<CredentialsTextInput
|
||||
ref={emailRef}
|
||||
className={styles['credentials-text-input']}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@
|
|||
color: var(--primary-foreground-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-button, .guest-login-button, .signup-form-button, .login-form-button {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { useLocation, useParams, useNavigate } = require('react-router');
|
||||
const { useSearchParams } = require('react-router-dom');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const NotFound = require('stremio/routes/NotFound');
|
||||
const { useProfile, useNotifications, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
|
||||
const { DelayedRenderer, Chips, Image, MainNavBars, Multiselect, LibItem } = require('stremio/components');
|
||||
const { DelayedRenderer, Chips, Image, MainNavBars, LibItem, MultiselectMenu } = require('stremio/components');
|
||||
const { default: Placeholder } = require('./Placeholder');
|
||||
const useLibrary = require('./useLibrary');
|
||||
const useSelectableInputs = require('./useSelectableInputs');
|
||||
|
|
@ -43,6 +44,7 @@ const Library = ({ model }) => {
|
|||
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 (
|
||||
<MainNavBars className={styles['library-container']} route={model}>
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<div className={styles['library-content']}>
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
<Multiselect {...typeSelect} className={styles['select-input-container']} />
|
||||
<MultiselectMenu {...typeSelect} className={styles['select-input-container']} />
|
||||
<Chips {...sortChips} className={styles['select-input-container']} />
|
||||
</div>
|
||||
{
|
||||
|
|
@ -82,7 +84,7 @@ const Library = ({ model }) => {
|
|||
src={require('/images/empty.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['message-label']}>{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!</div>
|
||||
<div className={styles['message-label']}>{model === 'library' ? t('LIBRARY_NOT_LOADED') : t('BOARD_CONTINUE_WATCHING_NOT_LOADED')}</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
|
|
@ -93,7 +95,7 @@ const Library = ({ model }) => {
|
|||
src={require('/images/empty.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['message-label']}>Empty {model === 'library' ? 'Library' : 'Continue Watching'}</div>
|
||||
<div className={styles['message-label']}>{model === 'library' ? t('LIBRARY_EMPTY') : t('BOARD_CONTINUE_WATCHING_EMPTY')}</div>
|
||||
</div>
|
||||
:
|
||||
<div ref={scrollContainerRef} className={classnames(styles['meta-items-container'], 'animation-fade-in')} onScroll={onScroll}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<DelayedRenderer delay={500}>
|
||||
<div className={styles['meta-message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No meta was selected!</div>
|
||||
<div className={styles['message-label']}>{t('ERR_NO_META_SELECTED')}</div>
|
||||
</div>
|
||||
</DelayedRenderer>
|
||||
:
|
||||
metaDetails.metaItem === null ?
|
||||
<div className={styles['meta-message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No addons were requested for this meta!</div>
|
||||
<div className={styles['message-label']}>{t('ERR_NO_ADDONS_FOR_META')}</div>
|
||||
</div>
|
||||
:
|
||||
metaDetails.metaItem.content.type === 'Err' ?
|
||||
<div className={styles['meta-message-container']}>
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['message-label']}>No metadata was found!</div>
|
||||
<div className={styles['message-label']}>{t('ERR_NO_META_FOUND')}</div>
|
||||
</div>
|
||||
:
|
||||
metaDetails.metaItem.content.type === 'Loading' ?
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
{...selectableOptions}
|
||||
className={styles['select-input-container']}
|
||||
/>
|
||||
|
|
@ -135,7 +134,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
|
|||
: null
|
||||
}
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['label']}>No addons were requested for streams!</div>
|
||||
<div className={styles['label']}>{t('ERR_NO_ADDONS_FOR_STREAMS')}</div>
|
||||
</div>
|
||||
:
|
||||
props.streams.every((streams) => streams.content.type === 'Err') ?
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={classnames(className, styles['seasons-bar-container'])}>
|
||||
<Button className={classnames(styles['prev-season-button'], { 'disabled': prevDisabled })} title={'Previous season'} data-action={'prev'} onClick={prevNextButtonOnClick}>
|
||||
<Button className={classnames(styles['prev-season-button'], { 'disabled': prevDisabled })} title={t('PREV_SEASON')} data-action={'prev'} onClick={prevNextButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||
<div className={styles['label']}>Prev</div>
|
||||
<div className={styles['label']}>{t('BUTTON_PREV')}</div>
|
||||
</Button>
|
||||
<MultiselectMenu
|
||||
className={styles['seasons-popup-label-container']}
|
||||
options={options}
|
||||
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
||||
selectedOption={selectedSeason}
|
||||
title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')}
|
||||
value={selectedSeason}
|
||||
onSelect={seasonOnSelect}
|
||||
/>
|
||||
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>
|
||||
<div className={styles['label']}>Next</div>
|
||||
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={t('NEXT_SEASON')} data-action={'next'} onClick={prevNextButtonOnClick}>
|
||||
<div className={styles['label']}>{t('BUTTON_NEXT')}</div>
|
||||
<Icon className={styles['icon']} name={'chevron-forward'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={classnames(className, styles['seasons-bar-placeholder-container'])}>
|
||||
<div className={styles['prev-season-button']}>
|
||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||
<div className={styles['label']}>Prev</div>
|
||||
<div className={styles['label']}>{t('SEASON_PREV')}</div>
|
||||
</div>
|
||||
<div className={styles['seasons-popup-label-container']}>
|
||||
<div className={styles['seasons-popup-label']}>Season 1</div>
|
||||
<div className={styles['seasons-popup-label']}>{t('SEASON_NUMBER', { season: 1 })}</div>
|
||||
<Icon className={styles['seasons-popup-icon']} name={'caret-down'} />
|
||||
</div>
|
||||
<div className={styles['next-season-button']}>
|
||||
<div className={styles['label']}>Next</div>
|
||||
<div className={styles['label']}>{t('SEASON_NEXT')}</div>
|
||||
<Icon className={styles['icon']} name={'chevron-forward'} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
|||
<div className={styles['message-container']}>
|
||||
<EpisodePicker className={styles['episode-picker']} onSubmit={onSeasonSearch} />
|
||||
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
|
||||
<div className={styles['label']}>No videos found for this meta!</div>
|
||||
<div className={styles['label']}>{t('ERR_NO_VIDEOS_FOR_META')}</div>
|
||||
</div>
|
||||
:
|
||||
<React.Fragment>
|
||||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={styles['not-found-container']}>
|
||||
<HorizontalNavBar
|
||||
className={styles['nav-bar']}
|
||||
title={'Page not found'}
|
||||
title={t('PAGE_NOT_FOUND')}
|
||||
backButton={true}
|
||||
fullscreenButton={true}
|
||||
navMenu={true}
|
||||
|
|
@ -20,7 +22,7 @@ const NotFound = () => {
|
|||
src={require('/images/empty.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['not-found-label']}>Page not found!</div>
|
||||
<div className={styles['not-found-label']}>{t('PAGE_NOT_FOUND')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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' ?
|
||||
<div className={styles['name']}>
|
||||
<span className={styles['label']}>Next on</span> { metaItem.name }
|
||||
<span className={styles['label']}>{t('PLAYER_NEXT_VIDEO_TITLE_SHORT')}</span> { metaItem.name }
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
@ -82,11 +84,11 @@ const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideo
|
|||
<div className={styles['buttons-container']}>
|
||||
<Button className={classnames(styles['button-container'], styles['dismiss'])} onClick={onDismissButtonClick}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
<div className={styles['label']}>Dismiss</div>
|
||||
<div className={styles['label']}>{t('PLAYER_NEXT_VIDEO_BUTTON_DISMISS')}</div>
|
||||
</Button>
|
||||
<Button ref={watchNowButtonRef} className={classnames(styles['button-container'], styles['play-button'])} onClick={onWatchNowButtonClick}>
|
||||
<Icon className={styles['icon']} name={'play'} />
|
||||
<div className={styles['label']}>Watch Now</div>
|
||||
<div className={styles['label']}>{t('PLAYER_NEXT_VIDEO_BUTTON_WATCH')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={classNames(className, styles['statistics-menu-container'])}>
|
||||
<div className={styles['title']}>
|
||||
Statistics
|
||||
{t('PLAYER_STATISTICS')}
|
||||
</div>
|
||||
<div className={styles['stats']}>
|
||||
<div className={styles['stat']}>
|
||||
<div className={styles['label']}>
|
||||
Peers
|
||||
{t('PLAYER_PEERS')}
|
||||
</div>
|
||||
<div className={styles['value']}>
|
||||
{ peers }
|
||||
|
|
@ -22,15 +24,15 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
|
|||
</div>
|
||||
<div className={styles['stat']}>
|
||||
<div className={styles['label']}>
|
||||
Speed
|
||||
{t('PLAYER_SPEED')}
|
||||
</div>
|
||||
<div className={styles['value']}>
|
||||
{ speed } MB/s
|
||||
{`${speed} ${t('MB_S')}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['stat']}>
|
||||
<div className={styles['label']}>
|
||||
Completed
|
||||
{t('PLAYER_COMPLETED')}
|
||||
</div>
|
||||
<div className={styles['value']}>
|
||||
{ Math.min(completed, 100) } %
|
||||
|
|
@ -39,7 +41,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
|
|||
</div>
|
||||
<div className={styles['info-hash']}>
|
||||
<div className={styles['label']}>
|
||||
Info Hash
|
||||
{t('PLAYER_INFO_HASH')}
|
||||
</div>
|
||||
<div className={styles['value']}>
|
||||
{ infoHash }
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={classnames(className, styles['discrete-input-container'], { 'disabled': disabled })}>
|
||||
<div className={styles['header']}>{label}</div>
|
||||
<div className={styles['input-container']} title={disabled ? `${label} is not configurable` : null}>
|
||||
<div className={styles['input-container']} title={disabled ? t('DISABLED_LABEL', { label }) : null}>
|
||||
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'decrement'} onClick={buttonOnClick}>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</Button>
|
||||
<div className={styles['spacing']} />
|
||||
<div className={styles['version-info-label']} title={process.env.VERSION}>
|
||||
App Version: {process.env.VERSION}
|
||||
{`${t('SETTINGS_APP_VERSION')}: ${process.env.VERSION}`}
|
||||
</div>
|
||||
<div className={styles['version-info-label']} title={process.env.COMMIT_HASH}>
|
||||
Build Version: {process.env.COMMIT_HASH}
|
||||
{`${t('SETTINGS_BUILD_VERSION')}: ${process.env.COMMIT_HASH}`}
|
||||
</div>
|
||||
{
|
||||
streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ?
|
||||
<div className={styles['version-info-label']} title={streamingServer.settings.content.serverVersion}>Server Version: {streamingServer.settings.content.serverVersion}</div>
|
||||
<div className={styles['version-info-label']} title={streamingServer.settings.content.serverVersion}>{`${t('SETTINGS_SERVER_VERSION')}: ${streamingServer.settings.content.serverVersion}`}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' ?
|
||||
<div className={styles['version-info-label']} title={shell.transport.props.shellVersion}>Shell Version: {shell.transport.props.shellVersion}</div>
|
||||
<div className={styles['version-info-label']} title={shell.transport.props.shellVersion}>{`${t('SETTINGS_APP_VERSION')}: ${shell.transport.props.shellVersion}`}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
|
@ -219,9 +219,9 @@ const Settings = () => {
|
|||
}}
|
||||
/>
|
||||
<div className={styles['email-logout-container']}>
|
||||
<div className={styles['email-label-container']} title={profile.auth === null ? 'Anonymous user' : profile.auth.user.email}>
|
||||
<div className={styles['email-label-container']} title={profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}>
|
||||
<div className={styles['email-label']}>
|
||||
{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}
|
||||
{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
|
|
@ -273,8 +273,8 @@ const Settings = () => {
|
|||
</Button>
|
||||
</div>
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={'Source code'} target={'_blank'} href={`https://github.com/stremio/stremio-web/tree/${process.env.COMMIT_HASH}`}>
|
||||
<div className={styles['label']}>Source code</div>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_SOURCE_CODE')} target={'_blank'} href={`https://github.com/stremio/stremio-web/tree/${process.env.COMMIT_HASH}`}>
|
||||
<div className={styles['label']}>{t('SETTINGS_SOURCE_CODE')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
|
|
@ -310,11 +310,11 @@ const Settings = () => {
|
|||
<div className={styles['option-container']}>
|
||||
<div className={classnames(styles['option-name-container'], styles['trakt-icon'])}>
|
||||
<Icon className={styles['icon']} name={'trakt'} />
|
||||
<div className={styles['label']}>Trakt Scrobbling</div>
|
||||
<div className={styles['label']}>{t('SETTINGS_TRAKT')}</div>
|
||||
</div>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Authenticate'} disabled={profile.auth === null} tabIndex={-1} onClick={toggleTraktOnClick}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={t('SETTINGS_TRAKT_AUTHENTICATE')} disabled={profile.auth === null} tabIndex={-1} onClick={toggleTraktOnClick}>
|
||||
<div className={styles['label']}>
|
||||
{ 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') }
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -324,7 +324,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_UI_LANGUAGE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
tabIndex={-1}
|
||||
{...interfaceLanguageSelect}
|
||||
|
|
@ -376,7 +376,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_LANGUAGE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...subtitlesLanguageSelect}
|
||||
/>
|
||||
|
|
@ -385,7 +385,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_SIZE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...subtitlesSizeSelect}
|
||||
/>
|
||||
|
|
@ -427,7 +427,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...audioLanguageSelect}
|
||||
/>
|
||||
|
|
@ -452,7 +452,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...seekTimeDurationSelect}
|
||||
/>
|
||||
|
|
@ -461,7 +461,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY_SHIFT') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...seekShortTimeDurationSelect}
|
||||
/>
|
||||
|
|
@ -496,7 +496,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
disabled={!profile.settings.bingeWatching}
|
||||
{...nextVideoPopupDurationSelect}
|
||||
|
|
@ -512,7 +512,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...playInExternalPlayerSelect}
|
||||
/>
|
||||
|
|
@ -568,7 +568,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_HTTPS_ENDPOINT') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...remoteEndpointSelect}
|
||||
/>
|
||||
|
|
@ -582,7 +582,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SERVER_CACHE_SIZE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...cacheSizeSelect}
|
||||
/>
|
||||
|
|
@ -596,7 +596,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SERVER_TORRENT_PROFILE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...torrentProfileSelect}
|
||||
/>
|
||||
|
|
@ -610,7 +610,7 @@ const Settings = () => {
|
|||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_TRANSCODE_PROFILE') }</div>
|
||||
</div>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...transcodingProfileSelect}
|
||||
/>
|
||||
|
|
@ -740,7 +740,7 @@ const Settings = () => {
|
|||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
App Version
|
||||
{t('SETTINGS_APP_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
|
|
@ -752,7 +752,7 @@ const Settings = () => {
|
|||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
Build Version
|
||||
{t('SETTINGS_BUILD_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
|
|
@ -766,7 +766,7 @@ const Settings = () => {
|
|||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
Server Version
|
||||
{t('SETTINGS_SERVER_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
|
|
@ -783,7 +783,7 @@ const Settings = () => {
|
|||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
Shell Version
|
||||
{t('SETTINGS_SHELL_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const URLsManager = () => {
|
|||
return (
|
||||
<div className={styles['wrapper']}>
|
||||
<div className={styles['header']}>
|
||||
<div className={styles['label']}>URL</div>
|
||||
<div className={styles['label']}>{t('URL')}</div>
|
||||
<div className={styles['label']}>{t('STATUS')}</div>
|
||||
</div>
|
||||
<div className={styles['content']}>
|
||||
|
|
@ -46,11 +46,11 @@ const URLsManager = () => {
|
|||
}
|
||||
</div>
|
||||
<div className={styles['footer']}>
|
||||
<Button title={'Add URL'} className={styles['add-url']} onClick={onAdd}>
|
||||
<Button title={t('SETTINGS_SERVER_ADD_URL')} className={styles['add-url']} onClick={onAdd}>
|
||||
<Icon name={'add'} className={styles['icon']} />
|
||||
{t('SETTINGS_SERVER_ADD_URL')}
|
||||
</Button>
|
||||
<Button className={styles['reload']} title={'Reload'} onClick={reloadServer}>
|
||||
<Button className={styles['reload']} title={t('RELOAD')} onClick={reloadServer}>
|
||||
<Icon name={'reset'} className={styles['icon']} />
|
||||
<div className={styles['label']}>{t('RELOAD')}</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -267,6 +267,11 @@
|
|||
|
||||
.option-input-container {
|
||||
padding: 1rem 1.5rem;
|
||||
|
||||
&.multiselect-container {
|
||||
padding: 0;
|
||||
background: var(--overlay-color);
|
||||
}
|
||||
|
||||
&.button-container {
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
src/types/models/Ctx.d.ts
vendored
2
src/types/models/Ctx.d.ts
vendored
|
|
@ -35,7 +35,7 @@ type Settings = {
|
|||
subtitlesBackgroundColor: string,
|
||||
subtitlesBold: boolean,
|
||||
subtitlesFont: string,
|
||||
subtitlesLanguage: string,
|
||||
subtitlesLanguage: string | null,
|
||||
subtitlesOffset: number,
|
||||
subtitlesOutlineColor: string,
|
||||
subtitlesSize: number,
|
||||
|
|
|
|||
107
tests/i18nScan.test.js
Normal file
107
tests/i18nScan.test.js
Normal file
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue