Merge branch 'development' into feat/untranslated-strings

This commit is contained in:
Botzy 2025-06-10 13:57:12 +03:00
commit 2f0ec456fe
27 changed files with 277 additions and 213 deletions

65
.github/workflows/auto_assign.yml vendored Normal file
View 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@v6
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@v6
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
});
}

View file

@ -102,12 +102,18 @@ const App = () => {
// Handle shell events // Handle shell events
React.useEffect(() => { React.useEffect(() => {
const onOpenMedia = (data) => { const onOpenMedia = (data) => {
if (data.startsWith('stremio:///')) return; try {
if (data.startsWith('stremio://')) { const { protocol, hostname, pathname, searchParams } = new URL(data);
const transportUrl = data.replace('stremio://', 'https://'); if (protocol === CONSTANTS.PROTOCOL) {
if (URL.canParse(transportUrl)) { if (hostname.length) {
window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`; 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);
} }
}; };

View file

@ -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 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 = { module.exports = {
CHROMECAST_RECEIVER_APP_ID, CHROMECAST_RECEIVER_APP_ID,
DEFAULT_STREAMING_SERVER_URL, DEFAULT_STREAMING_SERVER_URL,
@ -127,4 +129,5 @@ module.exports = {
SUPPORTED_LOCAL_SUBTITLES, SUPPORTED_LOCAL_SUBTITLES,
EXTERNAL_PLAYERS, EXTERNAL_PLAYERS,
WHITELISTED_HOSTS, WHITELISTED_HOSTS,
PROTOCOL,
}; };

View file

@ -1,6 +1,5 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
import useShell from 'stremio/common/useShell';
import { name, isMobile } from './device'; import { name, isMobile } from './device';
interface PlatformContext { interface PlatformContext {
@ -16,19 +15,13 @@ type Props = {
}; };
const PlatformProvider = ({ children }: Props) => { const PlatformProvider = ({ children }: Props) => {
const shell = useShell();
const openExternal = (url: string) => { const openExternal = (url: string) => {
try { try {
const { hostname } = new URL(url); const { hostname } = new URL(url);
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host)); const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url; const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
if (shell.active) { window.open(finalUrl, '_blank');
shell.send('open-external', finalUrl);
} else {
window.open(finalUrl, '_blank');
}
} catch (e) { } catch (e) {
console.error('Failed to parse external url:', e); console.error('Failed to parse external url:', e);
} }

View file

@ -6,7 +6,7 @@ const routesRegexp = {
urlParamsNames: [] urlParamsNames: []
}, },
board: { board: {
regexp: /^\/?$/, regexp: /^\/?(?:board)?$/,
urlParamsNames: [] urlParamsNames: []
}, },
discover: { discover: {

View file

@ -46,6 +46,10 @@ const useFullscreen = () => {
exitFullscreen(); exitFullscreen();
} }
if (event.code === 'KeyF') {
toggleFullscreen();
}
if (event.code === 'F11' && shell.active) { if (event.code === 'F11' && shell.active) {
toggleFullscreen(); toggleFullscreen();
} }
@ -60,7 +64,7 @@ const useFullscreen = () => {
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('fullscreenchange', onFullscreenChange); document.removeEventListener('fullscreenchange', onFullscreenChange);
}; };
}, [settings.escExitFullscreen]); }, [settings.escExitFullscreen, toggleFullscreen]);
return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen];
}; };

View file

@ -10,23 +10,25 @@ import styles from './Dropdown.less';
type Props = { type Props = {
options: MultiselectMenuOption[]; options: MultiselectMenuOption[];
selectedOption?: MultiselectMenuOption | null; value?: string | number;
menuOpen: boolean | (() => void); menuOpen: boolean | (() => void);
level: number; level: number;
setLevel: (level: number) => void; 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 { t } = useTranslation();
const optionsRef = useRef(new Map()); const optionsRef = useRef(new Map());
const containerRef = useRef(null); 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) { if (node) {
optionsRef.current.set(value, node); optionsRef.current.set(optionValue, node);
} else { } 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) .filter((option: MultiselectMenuOption) => !option.hidden)
.map((option: MultiselectMenuOption) => ( .map((option: MultiselectMenuOption) => (
<Option <Option
key={option.id} key={option.value}
ref={handleSetOptionRef(option.value)} ref={handleSetOptionRef(option.value)}
option={option} option={option}
onSelect={onSelect} onSelect={onSelect}
selectedOption={selectedOption} selectedValue={value}
/> />
)) ))
} }

View file

@ -8,13 +8,12 @@ import Icon from '@stremio/stremio-icons/react';
type Props = { type Props = {
option: MultiselectMenuOption; option: MultiselectMenuOption;
selectedOption?: MultiselectMenuOption | null; selectedValue?: string | number;
onSelect: (value: number) => void; onSelect: (value: string | number) => void;
}; };
const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedOption, onSelect }, ref) => { const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedValue, onSelect }, ref) => {
// consider using option.id === selectedOption?.id instead const selected = useMemo(() => option?.value === selectedValue, [option, selectedValue]);
const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
onSelect(option.value); onSelect(option.value);

View file

@ -14,14 +14,21 @@
} }
.multiselect-button { .multiselect-button {
color: var(--primary-foreground-color);
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
display: flex; display: flex;
flex: 1;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0 0.5rem; gap: 0 0.5rem;
border-radius: @border-radius; border-radius: @border-radius;
.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--primary-foreground-color);
}
.icon { .icon {
width: 1rem; width: 1rem;
color: var(--primary-foreground-color); color: var(--primary-foreground-color);
@ -33,7 +40,7 @@
} }
} }
&:hover { &:hover, &.active {
background-color: var(--overlay-color); background-color: var(--overlay-color);
} }
} }

View file

@ -11,23 +11,25 @@ import useOutsideClick from 'stremio/common/useOutsideClick';
type Props = { type Props = {
className?: string, className?: string,
title?: string; title?: string | (() => string);
options: MultiselectMenuOption[]; options: MultiselectMenuOption[];
selectedOption?: MultiselectMenuOption; value?: string | number;
onSelect: (value: number) => void; 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 [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const multiselectMenuRef = useOutsideClick(() => closeMenu()); const multiselectMenuRef = useOutsideClick(() => closeMenu());
const [level, setLevel] = React.useState<number>(0); const [level, setLevel] = React.useState<number>(0);
const onOptionSelect = (value: number) => { const selectedOption = options.find((opt) => opt.value === value);
level ? setLevel(level + 1) : onSelect(value), closeMenu();
const onOptionSelect = (selectedValue: string | number) => {
level ? setLevel(level + 1) : onSelect(selectedValue), closeMenu();
}; };
return ( return (
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}> <div className={classNames(styles['multiselect-menu'], { [styles['active']]: menuOpen }, className)} ref={multiselectMenuRef}>
<Button <Button
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })} className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
onClick={toggleMenu} onClick={toggleMenu}
@ -35,7 +37,13 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
aria-haspopup='listbox' aria-haspopup='listbox'
aria-expanded={menuOpen} 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 })} /> <Icon name={'caret-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
</Button> </Button>
{ {
@ -46,7 +54,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
options={options} options={options}
onSelect={onOptionSelect} onSelect={onOptionSelect}
menuOpen={menuOpen} menuOpen={menuOpen}
selectedOption={selectedOption} value={value}
/> />
: null : null
} }

View file

@ -6,7 +6,7 @@ const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common'); 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 { useServices } = require('stremio/services');
const Addon = require('./Addon'); const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons'); const useInstalledAddons = require('./useInstalledAddons');
@ -107,7 +107,7 @@ const Addons = ({ urlParams, queryParams }) => {
<div className={styles['addons-content']}> <div className={styles['addons-content']}>
<div className={styles['selectable-inputs-container']}> <div className={styles['selectable-inputs-container']}>
{selectInputs.map((selectInput, index) => ( {selectInputs.map((selectInput, index) => (
<Multiselect <MultiselectMenu
{...selectInput} {...selectInput}
key={index} key={index}
className={styles['select-input-container']} className={styles['select-input-container']}
@ -218,7 +218,7 @@ const Addons = ({ urlParams, queryParams }) => {
filtersModalOpen ? filtersModalOpen ?
<ModalDialog title={t('ADDONS_FILTERS')} className={styles['filters-modal']} onCloseRequest={closeFiltersModal}> <ModalDialog title={t('ADDONS_FILTERS')} className={styles['filters-modal']} onCloseRequest={closeFiltersModal}>
{selectInputs.map((selectInput, index) => ( {selectInputs.map((selectInput, index) => (
<Multiselect <MultiselectMenu
{...selectInput} {...selectInput}
key={index} key={index}
className={styles['select-input-container']} className={styles['select-input-container']}

View file

@ -90,6 +90,7 @@
} }
.select-input-container { .select-input-container {
background-color: var(--overlay-color);
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
flex-basis: 15rem; flex-basis: 15rem;

View file

@ -4,8 +4,8 @@ const React = require('react');
const { useTranslate } = require('stremio/common'); const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (installedAddons, remoteAddons, t) => { const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
const selectedCatalog = remoteAddons.selectable.catalogs.concat(installedAddons.selectable.catalogs).find(({ selected }) => selected);
const catalogSelect = { const catalogSelect = {
title: t.string('SELECT_CATALOG'),
options: remoteAddons.selectable.catalogs options: remoteAddons.selectable.catalogs
.concat(installedAddons.selectable.catalogs) .concat(installedAddons.selectable.catalogs)
.map(({ name, deepLinks }) => ({ .map(({ name, deepLinks }) => ({
@ -13,24 +13,22 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
label: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), label: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'),
title: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), title: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'),
})), })),
selected: remoteAddons.selectable.catalogs value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined,
.concat(installedAddons.selectable.catalogs) title: remoteAddons.selected !== null ?
.filter(({ selected }) => selected)
.map(({ deepLinks }) => deepLinks.addons),
renderLabelText: remoteAddons.selected !== null ?
() => { () => {
const selectableCatalog = remoteAddons.selectable.catalogs const selectableCatalog = remoteAddons.selectable.catalogs
.find(({ id }) => id === remoteAddons.selected.request.path.id); .find(({ id }) => id === remoteAddons.selected.request.path.id);
return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name, 'ADDON_') : remoteAddons.selected.request.path.id; return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name, 'ADDON_') : remoteAddons.selected.request.path.id;
} }
: : null,
null, onSelect: (value) => {
onSelect: (event) => { window.location = value;
window.location = event.value;
} }
}; };
const selectedType = installedAddons.selected !== null
? installedAddons.selectable.types.find(({ selected }) => selected)
: remoteAddons.selectable.types.find(({ selected }) => selected);
const typeSelect = { const typeSelect = {
title: t.string('SELECT_TYPE'),
options: installedAddons.selected !== null ? options: installedAddons.selected !== null ?
installedAddons.selectable.types.map(({ type, deepLinks }) => ({ installedAddons.selectable.types.map(({ type, deepLinks }) => ({
value: deepLinks.addons, value: deepLinks.addons,
@ -41,15 +39,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
value: deepLinks.addons, value: deepLinks.addons,
label: t.stringWithPrefix(type, 'TYPE_') label: t.stringWithPrefix(type, 'TYPE_')
})), })),
selected: installedAddons.selected !== null ? value: selectedType ? selectedType.deepLinks.addons : undefined,
installedAddons.selectable.types title: () => {
.filter(({ selected }) => selected)
.map(({ deepLinks }) => deepLinks.addons)
:
remoteAddons.selectable.types
.filter(({ selected }) => selected)
.map(({ deepLinks }) => deepLinks.addons),
renderLabelText: () => {
return installedAddons.selected !== null ? return installedAddons.selected !== null ?
installedAddons.selected.request.type === null ? installedAddons.selected.request.type === null ?
t.string('TYPE_ALL') t.string('TYPE_ALL')
@ -61,8 +52,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
: :
typeSelect.title; typeSelect.title;
}, },
onSelect: (event) => { onSelect: (value) => {
window.location = event.value; window.location = value;
} }
}; };
return [catalogSelect, typeSelect]; return [catalogSelect, typeSelect];

View file

@ -7,7 +7,7 @@ const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common'); 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 useDiscover = require('./useDiscover');
const useSelectableInputs = require('./useSelectableInputs'); const useSelectableInputs = require('./useSelectableInputs');
const styles = require('./styles'); const styles = require('./styles');
@ -102,14 +102,13 @@ const Discover = ({ urlParams, queryParams }) => {
<div className={styles['discover-content']}> <div className={styles['discover-content']}>
<div className={styles['catalog-container']}> <div className={styles['catalog-container']}>
<div className={styles['selectable-inputs-container']}> <div className={styles['selectable-inputs-container']}>
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => ( {selectInputs.map(({ title, options, value, onSelect }, index) => (
<Multiselect <MultiselectMenu
key={index} key={index}
className={styles['select-input']} className={styles['select-input']}
title={title} title={title}
options={options} options={options}
selected={selected} value={value}
renderLabelText={renderLabelText}
onSelect={onSelect} onSelect={onSelect}
/> />
))} ))}
@ -205,14 +204,13 @@ const Discover = ({ urlParams, queryParams }) => {
{ {
inputsModalOpen ? inputsModalOpen ?
<ModalDialog title={t('CATALOG_FILTERS')} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}> <ModalDialog title={t('CATALOG_FILTERS')} className={styles['selectable-inputs-modal']} onCloseRequest={closeInputsModal}>
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => ( {selectInputs.map(({ title, options, value, onSelect }, index) => (
<Multiselect <MultiselectMenu
key={index} key={index}
className={styles['select-input']} className={styles['select-input']}
title={title} title={title}
options={options} options={options}
selected={selected} value={value}
renderLabelText={renderLabelText}
onSelect={onSelect} onSelect={onSelect}
/> />
))} ))}

View file

@ -50,6 +50,7 @@
.select-input { .select-input {
flex: 0 1 15rem; flex: 0 1 15rem;
background-color: var(--overlay-color);
&:not(:first-child) { &:not(:first-child) {
margin-left: 1.5rem; margin-left: 1.5rem;

View file

@ -4,72 +4,70 @@ const React = require('react');
const { useTranslate } = require('stremio/common'); const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (discover, t) => { const mapSelectableInputs = (discover, t) => {
const selectedType = discover.selectable.types.find(({ selected }) => selected);
const typeSelect = { const typeSelect = {
title: t.string('SELECT_TYPE'),
options: discover.selectable.types options: discover.selectable.types
.map(({ type, deepLinks }) => ({ .map(({ type, deepLinks }) => ({
value: deepLinks.discover, value: deepLinks.discover,
label: t.stringWithPrefix(type, 'TYPE_') label: t.stringWithPrefix(type, 'TYPE_')
})), })),
selected: discover.selectable.types value: selectedType
.filter(({ selected }) => selected) ? selectedType.deepLinks.discover
.map(({ deepLinks }) => deepLinks.discover), : undefined,
renderLabelText: discover.selected !== null ? title: discover.selected !== null
() => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_') ? () => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_')
: : t.string('SELECT_TYPE'),
null, onSelect: (value) => {
onSelect: (event) => { window.location = value;
window.location = event.value;
} }
}; };
const selectedCatalog = discover.selectable.catalogs.find(({ selected }) => selected);
const catalogSelect = { const catalogSelect = {
title: t.string('SELECT_CATALOG'),
options: discover.selectable.catalogs options: discover.selectable.catalogs
.map(({ id, name, addon, deepLinks }) => ({ .map(({ id, name, addon, deepLinks }) => ({
value: deepLinks.discover, value: deepLinks.discover,
label: t.catalogTitle({ addon, id, name }), label: t.catalogTitle({ addon, id, name }),
title: `${name} (${addon.manifest.name})` title: `${name} (${addon.manifest.name})`
})), })),
selected: discover.selectable.catalogs value: discover.selected?.request.path.id
.filter(({ selected }) => selected) ? selectedCatalog.deepLinks.discover
.map(({ deepLinks }) => deepLinks.discover), : undefined,
renderLabelText: discover.selected !== null ? title: discover.selected !== null
() => { ? () => {
const selectableCatalog = discover.selectable.catalogs const selectableCatalog = discover.selectable.catalogs
.find(({ id }) => id === discover.selected.request.path.id); .find(({ id }) => id === discover.selected.request.path.id);
return selectableCatalog ? t.catalogTitle(selectableCatalog, false) : discover.selected.request.path.id; return selectableCatalog ? t.catalogTitle(selectableCatalog, false) : discover.selected.request.path.id;
} }
: :
null, t.string('SELECT_CATALOG'),
onSelect: (event) => { onSelect: (value) => {
window.location = event.value; window.location =value;
} }
}; };
const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({ const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => {
title: t.string(name), const selectedExtra = options.find(({ selected }) => selected);
isRequired: isRequired, return {
options: options.map(({ value, deepLinks }) => ({ isRequired: isRequired,
label: typeof value === 'string' ? t.string(value) : t.string('NONE'), options: options.map(({ value, deepLinks }) => ({
value: JSON.stringify({ label: typeof value === 'string' ? t.string(value) : t.string('NONE'),
href: deepLinks.discover, value: JSON.stringify({
value href: deepLinks.discover,
}) value
})), })
selected: options
.filter(({ selected }) => selected)
.map(({ value, deepLinks }) => JSON.stringify({
href: deepLinks.discover,
value
})), })),
renderLabelText: options.some(({ selected, value }) => selected && value === null) ? value: JSON.stringify({
() => t.stringWithPrefix(name.toUpperCase(), 'SELECT_') href: selectedExtra.deepLinks.discover,
: value: selectedExtra.value,
null, }),
onSelect: (event) => { title: options.some(({ selected, value }) => selected && value === null) ?
const { href } = JSON.parse(event.value); () => t.string(name.toUpperCase())
window.location = href; : t.string(selectedExtra.value),
} onSelect: (value) => {
})); const { href } = JSON.parse(value);
window.location = href;
}
};
});
return [[typeSelect, catalogSelect, ...extraSelects], discover.selectable.nextPage]; return [[typeSelect, catalogSelect, ...extraSelects], discover.selectable.nextPage];
}; };

View file

@ -6,7 +6,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const NotFound = require('stremio/routes/NotFound'); const NotFound = require('stremio/routes/NotFound');
const { useProfile, useNotifications, routesRegexp, useOnScrollToBottom, withCoreSuspender } = require('stremio/common'); const { useProfile, useNotifications, routesRegexp, 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 { default: Placeholder } = require('./Placeholder');
const useLibrary = require('./useLibrary'); const useLibrary = require('./useLibrary');
const useSelectableInputs = require('./useSelectableInputs'); const useSelectableInputs = require('./useSelectableInputs');
@ -66,17 +66,17 @@ const Library = ({ model, urlParams, queryParams }) => {
} }
}, [profile.auth, library.selected]); }, [profile.auth, library.selected]);
React.useEffect(() => { React.useEffect(() => {
if (!library.selected?.type && typeSelect.selected) { if (!library.selected?.type && typeSelect.value) {
window.location = typeSelect.selected[0]; window.location = typeSelect.value;
} }
}, [typeSelect.selected, library.selected]); }, [typeSelect.value, library.selected]);
return ( return (
<MainNavBars className={styles['library-container']} route={model}> <MainNavBars className={styles['library-container']} route={model}>
{ {
profile.auth !== null ? profile.auth !== null ?
<div className={styles['library-content']}> <div className={styles['library-content']}>
<div className={styles['selectable-inputs-container']}> <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']} /> <Chips {...sortChips} className={styles['select-input-container']} />
</div> </div>
{ {

View file

@ -42,6 +42,7 @@
flex-shrink: 1; flex-shrink: 1;
flex-basis: 15rem; flex-basis: 15rem;
height: 2.75rem; height: 2.75rem;
background-color: var(--overlay-color);
&:not(:last-child) { &:not(:last-child) {
margin-right: 1.5rem; margin-right: 1.5rem;

View file

@ -2,22 +2,17 @@
const React = require('react'); const React = require('react');
const { useTranslate } = require('stremio/common'); const { useTranslate } = require('stremio/common');
const mapSelectableInputs = (library, t) => { const mapSelectableInputs = (library, t) => {
const selectedType = library.selectable.types const selectedType = library.selectable.types.find(({ selected }) => selected) || library.selectable.types.find(({ type }) => type === null);
.filter(({ selected }) => selected).map(({ deepLinks }) => deepLinks.library);
const typeSelect = { const typeSelect = {
title: t.string('SELECT_TYPE'),
options: library.selectable.types options: library.selectable.types
.map(({ type, deepLinks }) => ({ .map(({ type, deepLinks }) => ({
value: deepLinks.library, value: deepLinks.library,
label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_') label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_')
})), })),
selected: selectedType.length value: selectedType?.deepLinks.library,
? selectedType onSelect: (value) => {
: [library.selectable.types[0]].map(({ deepLinks }) => deepLinks.library), window.location = value;
onSelect: (event) => {
window.location = event.value;
} }
}; };
const sortChips = { const sortChips = {

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react'); 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 { useServices } = require('stremio/services');
const Stream = require('./Stream'); const Stream = require('./Stream');
const styles = require('./styles'); const styles = require('./styles');
@ -21,9 +21,9 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
const profile = useProfile(); const profile = useProfile();
const streamsContainerRef = React.useRef(null); const streamsContainerRef = React.useRef(null);
const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY); 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' }); streamsContainerRef.current.scrollTo({ top: 0, left: 0, behavior: platform.name === 'ios' ? 'smooth' : 'instant' });
setSelectedAddon(event.value); setSelectedAddon(value);
}, [platform]); }, [platform]);
const showInstallAddonsButton = React.useMemo(() => { const showInstallAddonsButton = React.useMemo(() => {
return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true && !video?.upcoming; return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true && !video?.upcoming;
@ -77,7 +77,6 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
}, [streamsByAddon, selectedAddon]); }, [streamsByAddon, selectedAddon]);
const selectableOptions = React.useMemo(() => { const selectableOptions = React.useMemo(() => {
return { return {
title: 'Select Addon',
options: [ options: [
{ {
value: ALL_ADDONS_KEY, value: ALL_ADDONS_KEY,
@ -90,7 +89,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
title: streamsByAddon[transportUrl].addon.manifest.name, title: streamsByAddon[transportUrl].addon.manifest.name,
})) }))
], ],
selected: [selectedAddon], value: selectedAddon,
onSelect: onAddonSelected onSelect: onAddonSelected
}; };
}, [streamsByAddon, selectedAddon]); }, [streamsByAddon, selectedAddon]);
@ -117,7 +116,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
} }
{ {
Object.keys(streamsByAddon).length > 1 ? Object.keys(streamsByAddon).length > 1 ?
<Multiselect <MultiselectMenu
{...selectableOptions} {...selectableOptions}
className={styles['select-input-container']} className={styles['select-input-container']}
/> />

View file

@ -114,7 +114,7 @@
.select-input-container { .select-input-container {
min-width: 40%; min-width: 40%;
flex-grow: 1; flex-grow: 1;
background: none; background-color: none;
&:hover, &:focus, &:global(.active) { &:hover, &:focus, &:global(.active) {
background-color: var(--overlay-color); background-color: var(--overlay-color);

View file

@ -17,7 +17,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
})); }));
}, [seasons]); }, [seasons]);
const selectedSeason = React.useMemo(() => { const selectedSeason = React.useMemo(() => {
return { label: String(season), value: String(season) }; return String(season);
}, [season]); }, [season]);
const prevNextButtonOnClick = React.useCallback((event) => { const prevNextButtonOnClick = React.useCallback((event) => {
if (typeof onSelect === 'function') { if (typeof onSelect === 'function') {
@ -64,7 +64,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
className={styles['seasons-popup-label-container']} className={styles['seasons-popup-label-container']}
options={options} options={options}
title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')} title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')}
selectedOption={selectedSeason} value={selectedSeason}
onSelect={seasonOnSelect} onSelect={seasonOnSelect}
/> />
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={t('NEXT_SEASON')} data-action={'next'} onClick={prevNextButtonOnClick}> <Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={t('NEXT_SEASON')} data-action={'next'} onClick={prevNextButtonOnClick}>

View file

@ -8,7 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const { useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common'); 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 useProfileSettingsInputs = require('./useProfileSettingsInputs');
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
const useDataExport = require('./useDataExport'); const useDataExport = require('./useDataExport');
@ -314,7 +314,7 @@ const Settings = () => {
</div> </div>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={t('SETTINGS_TRAKT_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']}> <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> </div>
</Button> </Button>
</div> </div>
@ -324,7 +324,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_UI_LANGUAGE') }</div> <div className={styles['label']}>{ t('SETTINGS_UI_LANGUAGE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
tabIndex={-1} tabIndex={-1}
{...interfaceLanguageSelect} {...interfaceLanguageSelect}
@ -376,7 +376,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_LANGUAGE') }</div> <div className={styles['label']}>{ t('SETTINGS_SUBTITLES_LANGUAGE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...subtitlesLanguageSelect} {...subtitlesLanguageSelect}
/> />
@ -385,7 +385,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_SIZE') }</div> <div className={styles['label']}>{ t('SETTINGS_SUBTITLES_SIZE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...subtitlesSizeSelect} {...subtitlesSizeSelect}
/> />
@ -427,7 +427,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }</div> <div className={styles['label']}>{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...audioLanguageSelect} {...audioLanguageSelect}
/> />
@ -452,7 +452,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY') }</div> <div className={styles['label']}>{ t('SETTINGS_SEEK_KEY') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...seekTimeDurationSelect} {...seekTimeDurationSelect}
/> />
@ -461,7 +461,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY_SHIFT') }</div> <div className={styles['label']}>{ t('SETTINGS_SEEK_KEY_SHIFT') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...seekShortTimeDurationSelect} {...seekShortTimeDurationSelect}
/> />
@ -496,7 +496,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }</div> <div className={styles['label']}>{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
disabled={!profile.settings.bingeWatching} disabled={!profile.settings.bingeWatching}
{...nextVideoPopupDurationSelect} {...nextVideoPopupDurationSelect}
@ -512,7 +512,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div> <div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...playInExternalPlayerSelect} {...playInExternalPlayerSelect}
/> />
@ -568,7 +568,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_HTTPS_ENDPOINT') }</div> <div className={styles['label']}>{ t('SETTINGS_HTTPS_ENDPOINT') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...remoteEndpointSelect} {...remoteEndpointSelect}
/> />
@ -582,7 +582,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SERVER_CACHE_SIZE') }</div> <div className={styles['label']}>{ t('SETTINGS_SERVER_CACHE_SIZE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...cacheSizeSelect} {...cacheSizeSelect}
/> />
@ -596,7 +596,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SERVER_TORRENT_PROFILE') }</div> <div className={styles['label']}>{ t('SETTINGS_SERVER_TORRENT_PROFILE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...torrentProfileSelect} {...torrentProfileSelect}
/> />
@ -610,7 +610,7 @@ const Settings = () => {
<div className={styles['option-name-container']}> <div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_TRANSCODE_PROFILE') }</div> <div className={styles['label']}>{ t('SETTINGS_TRANSCODE_PROFILE') }</div>
</div> </div>
<Multiselect <MultiselectMenu
className={classnames(styles['option-input-container'], styles['multiselect-container'])} className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...transcodingProfileSelect} {...transcodingProfileSelect}
/> />

View file

@ -267,6 +267,11 @@
.option-input-container { .option-input-container {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
&.multiselect-container {
padding: 0;
background: var(--overlay-color);
}
&.button-container { &.button-container {
justify-content: center; justify-content: center;

View file

@ -15,17 +15,15 @@ const useProfileSettingsInputs = (profile) => {
value: codes[0], value: codes[0],
label: name, label: name,
})), })),
selected: [ value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage,
interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage onSelect: (value) => {
],
onSelect: (event) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
interfaceLanguage: event.value interfaceLanguage: value
} }
} }
}); });
@ -72,15 +70,15 @@ const useProfileSettingsInputs = (profile) => {
label: languageNames[code] label: languageNames[code]
})) }))
], ],
selected: [profile.settings.subtitlesLanguage], value: profile.settings.subtitlesLanguage,
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
subtitlesLanguage: event.value subtitlesLanguage: value
} }
} }
}); });
@ -91,18 +89,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`, value: `${size}`,
label: `${size}%` label: `${size}%`
})), })),
selected: [`${profile.settings.subtitlesSize}`], value: `${profile.settings.subtitlesSize}`,
renderLabelText: () => { title: () => {
return `${profile.settings.subtitlesSize}%`; return `${profile.settings.subtitlesSize}%`;
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
subtitlesSize: parseInt(event.value, 10) subtitlesSize: parseInt(value, 10)
} }
} }
}); });
@ -110,14 +108,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]); }), [profile.settings]);
const subtitlesTextColorInput = React.useMemo(() => ({ const subtitlesTextColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesTextColor, value: profile.settings.subtitlesTextColor,
onChange: (event) => { onChange: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
subtitlesTextColor: event.value subtitlesTextColor: value
} }
} }
}); });
@ -125,14 +123,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]); }), [profile.settings]);
const subtitlesBackgroundColorInput = React.useMemo(() => ({ const subtitlesBackgroundColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesBackgroundColor, value: profile.settings.subtitlesBackgroundColor,
onChange: (event) => { onChange: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
subtitlesBackgroundColor: event.value subtitlesBackgroundColor: value
} }
} }
}); });
@ -140,14 +138,14 @@ const useProfileSettingsInputs = (profile) => {
}), [profile.settings]); }), [profile.settings]);
const subtitlesOutlineColorInput = React.useMemo(() => ({ const subtitlesOutlineColorInput = React.useMemo(() => ({
value: profile.settings.subtitlesOutlineColor, value: profile.settings.subtitlesOutlineColor,
onChange: (event) => { onChange: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
subtitlesOutlineColor: event.value subtitlesOutlineColor: value
} }
} }
}); });
@ -158,15 +156,15 @@ const useProfileSettingsInputs = (profile) => {
value: code, value: code,
label: languageNames[code] label: languageNames[code]
})), })),
selected: [profile.settings.audioLanguage], value: profile.settings.audioLanguage,
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
audioLanguage: event.value audioLanguage: value
} }
} }
}); });
@ -208,18 +206,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`, value: `${size}`,
label: `${size / 1000} ${t('SECONDS')}` label: `${size / 1000} ${t('SECONDS')}`
})), })),
selected: [`${profile.settings.seekTimeDuration}`], value: `${profile.settings.seekTimeDuration}`,
renderLabelText: () => { title: () => {
return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`; return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`;
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
seekTimeDuration: parseInt(event.value, 10) seekTimeDuration: parseInt(value, 10)
} }
} }
}); });
@ -230,18 +228,18 @@ const useProfileSettingsInputs = (profile) => {
value: `${size}`, value: `${size}`,
label: `${size / 1000} ${t('SECONDS')}` label: `${size / 1000} ${t('SECONDS')}`
})), })),
selected: [`${profile.settings.seekShortTimeDuration}`], value: `${profile.settings.seekShortTimeDuration}`,
renderLabelText: () => { title: () => {
return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`; return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`;
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
seekShortTimeDuration: parseInt(event.value, 10) seekShortTimeDuration: parseInt(value, 10)
} }
} }
}); });
@ -254,19 +252,19 @@ const useProfileSettingsInputs = (profile) => {
value, value,
label: t(label), label: t(label),
})), })),
selected: [profile.settings.playerType], value: profile.settings.playerType,
renderLabelText: () => { title: () => {
const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType); const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType);
return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType; return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType;
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
playerType: event.value playerType: value
} }
} }
}); });
@ -277,21 +275,21 @@ const useProfileSettingsInputs = (profile) => {
value: `${duration}`, value: `${duration}`,
label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}` label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}`
})), })),
selected: [`${profile.settings.nextVideoNotificationDuration}`], value: `${profile.settings.nextVideoNotificationDuration}`,
renderLabelText: () => { title: () => {
return profile.settings.nextVideoNotificationDuration === 0 ? return profile.settings.nextVideoNotificationDuration === 0 ?
'Disabled' 'Disabled'
: :
`${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`; `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`;
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...profile.settings, ...profile.settings,
nextVideoNotificationDuration: parseInt(event.value, 10) nextVideoNotificationDuration: parseInt(value, 10)
} }
} }
}); });

View file

@ -77,15 +77,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
value: address, value: address,
})) }))
], ],
selected: [streamingServer.settings.content.remoteHttps], value: streamingServer.settings.content.remoteHttps,
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'StreamingServer', action: 'StreamingServer',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...streamingServer.settings.content, ...streamingServer.settings.content,
remoteHttps: event.value, remoteHttps: value,
} }
} }
}); });
@ -103,18 +103,18 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
label: cacheSizeToString(size), label: cacheSizeToString(size),
value: JSON.stringify(size) value: JSON.stringify(size)
})), })),
selected: [JSON.stringify(streamingServer.settings.content.cacheSize)], value: JSON.stringify(streamingServer.settings.content.cacheSize),
renderLabelText: () => { title: () => {
return cacheSizeToString(streamingServer.settings.content.cacheSize); return cacheSizeToString(streamingServer.settings.content.cacheSize);
}, },
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'StreamingServer', action: 'StreamingServer',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...streamingServer.settings.content, ...streamingServer.settings.content,
cacheSize: JSON.parse(event.value), cacheSize: JSON.parse(value),
} }
} }
}); });
@ -152,15 +152,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
: :
[] []
), ),
selected: [JSON.stringify(selectedTorrentProfile)], value: JSON.stringify(selectedTorrentProfile),
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'StreamingServer', action: 'StreamingServer',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...streamingServer.settings.content, ...streamingServer.settings.content,
...JSON.parse(event.value), ...JSON.parse(value),
} }
} }
}); });
@ -183,15 +183,15 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
value: name, value: name,
})) }))
], ],
selected: [streamingServer.settings.content.transcodeProfile], value: streamingServer.settings.content.transcodeProfile,
onSelect: (event) => { onSelect: (value) => {
core.transport.dispatch({ core.transport.dispatch({
action: 'StreamingServer', action: 'StreamingServer',
args: { args: {
action: 'UpdateSettings', action: 'UpdateSettings',
args: { args: {
...streamingServer.settings.content, ...streamingServer.settings.content,
transcodeProfile: event.value, transcodeProfile: value,
} }
} }
}); });

View file

@ -56,16 +56,6 @@ function KeyboardShortcuts() {
window.history.back(); window.history.back();
} }
break;
}
case 'KeyF': {
event.preventDefault();
if (document.fullscreenElement === document.documentElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
break; break;
} }
} }