mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/react-router
This commit is contained in:
commit
d33f78ee39
85 changed files with 1961 additions and 1525 deletions
35
package-lock.json
generated
35
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#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
@ -50,6 +50,8 @@
|
|||
"@stylistic/eslint-plugin": "^2.11.0",
|
||||
"@stylistic/eslint-plugin-jsx": "^2.11.0",
|
||||
"@types/hat": "^0.0.4",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^18.3.13",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"babel-loader": "9.2.1",
|
||||
|
|
@ -3818,6 +3820,33 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz",
|
||||
"integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash.isequal": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
|
||||
"integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.throttle": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz",
|
||||
"integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
|
@ -13448,8 +13477,8 @@
|
|||
},
|
||||
"node_modules/stremio-translations": {
|
||||
"version": "1.44.12",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"integrity": "sha512-b38OjGwlsvFm/aNn/ia18mPxPjZvnI/GaToppn1XaQqCuZuSHxQlYDddwOYTztskWo4VO/IZmCi3UFewqpsqCQ==",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
|
|
|
|||
|
|
@ -43,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#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
@ -55,6 +55,8 @@
|
|||
"@stylistic/eslint-plugin": "^2.11.0",
|
||||
"@stylistic/eslint-plugin-jsx": "^2.11.0",
|
||||
"@types/hat": "^0.0.4",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^18.3.13",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"babel-loader": "9.2.1",
|
||||
|
|
|
|||
11
src/common/Toast/useToast.d.ts
vendored
Normal file
11
src/common/Toast/useToast.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
type ToastOptions = {
|
||||
type: string,
|
||||
title: string,
|
||||
timeout: number,
|
||||
};
|
||||
|
||||
declare const useToast: () => {
|
||||
show: (options: ToastOptions) => void,
|
||||
};
|
||||
|
||||
export = useToast;
|
||||
|
|
@ -82,6 +82,19 @@
|
|||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0);
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fade-in-no-motion {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const useModelState = require('./useModelState');
|
|||
const useNotifications = require('./useNotifications');
|
||||
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
||||
const useProfile = require('./useProfile');
|
||||
const { default: useRouteFocused } = require('./useRouteFocused');
|
||||
const { default: useSettings } = require('./useSettings');
|
||||
const { default: useShell } = require('./useShell');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
|
|
@ -53,6 +54,7 @@ module.exports = {
|
|||
useNotifications,
|
||||
useOnScrollToBottom,
|
||||
useProfile,
|
||||
useRouteFocused,
|
||||
useSettings,
|
||||
useShell,
|
||||
useStreamingServer,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import styles from './Button.less';
|
|||
|
||||
type Props = {
|
||||
className?: string,
|
||||
style?: object,
|
||||
href?: string,
|
||||
target?: string
|
||||
title?: string,
|
||||
|
|
|
|||
|
|
@ -1,55 +1,62 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const AColorPicker = require('a-color-picker');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { Button } = require('stremio/components');
|
||||
const ModalDialog = require('stremio/components/ModalDialog');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const ColorPicker = require('./ColorPicker');
|
||||
const styles = require('./styles');
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as AColorPicker from 'a-color-picker';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'stremio/components';
|
||||
import ModalDialog from 'stremio/components/ModalDialog';
|
||||
import useBinaryState from 'stremio/common/useBinaryState';
|
||||
import ColorPicker from './ColorPicker';
|
||||
import styles from './ColorInput.less';
|
||||
|
||||
const parseColor = (value) => {
|
||||
const parseColor = (value: string) => {
|
||||
const color = AColorPicker.parseColor(value, 'hexcss4');
|
||||
return typeof color === 'string' ? color : '#ffffffff';
|
||||
};
|
||||
|
||||
const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
|
||||
type Props = {
|
||||
className: string,
|
||||
value: string,
|
||||
onChange?: (value: string) => void,
|
||||
onClick?: (event: React.MouseEvent) => void,
|
||||
};
|
||||
|
||||
const ColorInput = ({ className, value, onChange, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [modalOpen, openModal, closeModal] = useBinaryState(false);
|
||||
const [tempValue, setTempValue] = React.useState(() => {
|
||||
const [tempValue, setTempValue] = useState(() => {
|
||||
return parseColor(value);
|
||||
});
|
||||
const labelButtonStyle = React.useMemo(() => ({
|
||||
|
||||
const labelButtonStyle = useMemo(() => ({
|
||||
backgroundColor: value
|
||||
}), [value]);
|
||||
const isTransparent = React.useMemo(() => {
|
||||
|
||||
const isTransparent = useMemo(() => {
|
||||
return parseColor(value).endsWith('00');
|
||||
}, [value]);
|
||||
const labelButtonOnClick = React.useCallback((event) => {
|
||||
|
||||
const labelButtonOnClick = useCallback((event: React.MouseEvent) => {
|
||||
if (typeof props.onClick === 'function') {
|
||||
props.onClick(event);
|
||||
}
|
||||
|
||||
// @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'.
|
||||
if (!event.nativeEvent.openModalPrevented) {
|
||||
openModal();
|
||||
}
|
||||
}, [props.onClick]);
|
||||
const modalDialogOnClick = React.useCallback((event) => {
|
||||
|
||||
const modalDialogOnClick = useCallback((event: React.MouseEvent) => {
|
||||
// @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'.
|
||||
event.nativeEvent.openModalPrevented = true;
|
||||
}, []);
|
||||
const modalButtons = React.useMemo(() => {
|
||||
const selectButtonOnClick = (event) => {
|
||||
|
||||
const modalButtons = useMemo(() => {
|
||||
const selectButtonOnClick = () => {
|
||||
if (typeof onChange === 'function') {
|
||||
onChange({
|
||||
type: 'change',
|
||||
value: tempValue,
|
||||
dataset: dataset,
|
||||
reactEvent: event,
|
||||
nativeEvent: event.nativeEvent
|
||||
});
|
||||
onChange(tempValue);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
|
|
@ -63,13 +70,16 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
|
|||
}
|
||||
}
|
||||
];
|
||||
}, [tempValue, dataset, onChange]);
|
||||
const colorPickerOnInput = React.useCallback((event) => {
|
||||
setTempValue(parseColor(event.value));
|
||||
}, [tempValue, onChange]);
|
||||
|
||||
const colorPickerOnInput = useCallback((color: string) => {
|
||||
setTempValue(parseColor(color));
|
||||
}, []);
|
||||
React.useLayoutEffect(() => {
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setTempValue(parseColor(value));
|
||||
}, [value, modalOpen]);
|
||||
|
||||
return (
|
||||
<Button title={isTransparent ? t('BUTTON_COLOR_TRANSPARENT') : value} {...props} style={labelButtonStyle} className={classnames(className, styles['color-input-container'])} onClick={labelButtonOnClick}>
|
||||
{
|
||||
|
|
@ -92,12 +102,4 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
|
|||
);
|
||||
};
|
||||
|
||||
ColorInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
dataset: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = ColorInput;
|
||||
export default ColorInput;
|
||||
|
|
@ -29,10 +29,7 @@ const ColorPicker = ({ className, value, onInput }) => {
|
|||
React.useLayoutEffect(() => {
|
||||
if (typeof onInput === 'function') {
|
||||
pickerRef.current.on('change', (picker, value) => {
|
||||
onInput({
|
||||
type: 'input',
|
||||
value: parseColor(value)
|
||||
});
|
||||
onInput(parseColor(value));
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const ColorInput = require('./ColorInput');
|
||||
|
||||
module.exports = ColorInput;
|
||||
|
||||
6
src/components/ColorInput/index.ts
Normal file
6
src/components/ColorInput/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
import ColorInput from './ColorInput';
|
||||
|
||||
export default ColorInput;
|
||||
|
||||
|
|
@ -10,11 +10,11 @@ import styles from './Dropdown.less';
|
|||
|
||||
type Props = {
|
||||
options: MultiselectMenuOption[];
|
||||
value?: string | number;
|
||||
value?: any;
|
||||
menuOpen: boolean | (() => void);
|
||||
level: number;
|
||||
setLevel: (level: number) => void;
|
||||
onSelect: (value: string | number) => void;
|
||||
onSelect: (value: any) => void;
|
||||
};
|
||||
|
||||
const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => {
|
||||
|
|
@ -24,7 +24,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props
|
|||
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
|
||||
const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => {
|
||||
const handleSetOptionRef = useCallback((optionValue: any) => (node: HTMLButtonElement | null) => {
|
||||
if (node) {
|
||||
optionsRef.current.set(optionValue, node);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import Icon from '@stremio/stremio-icons/react';
|
|||
|
||||
type Props = {
|
||||
option: MultiselectMenuOption;
|
||||
selectedValue?: string | number;
|
||||
onSelect: (value: string | number) => void;
|
||||
selectedValue?: any;
|
||||
onSelect: (value: any) => void;
|
||||
};
|
||||
|
||||
const Option = forwardRef<HTMLButtonElement, Props>(({ option, selectedValue, onSelect }, ref) => {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ import useOutsideClick from 'stremio/common/useOutsideClick';
|
|||
|
||||
type Props = {
|
||||
className?: string,
|
||||
title?: string | (() => string);
|
||||
title?: string | (() => string | null);
|
||||
options: MultiselectMenuOption[];
|
||||
value?: string | number;
|
||||
onSelect: (value: string | number) => void;
|
||||
value?: any;
|
||||
disabled?: boolean,
|
||||
onSelect: (value: any) => void;
|
||||
};
|
||||
|
||||
const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => {
|
||||
const MultiselectMenu = ({ className, title, options, value, disabled, onSelect }: Props) => {
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const multiselectMenuRef = useOutsideClick(() => closeMenu());
|
||||
const [level, setLevel] = React.useState<number>(0);
|
||||
|
|
@ -32,6 +33,7 @@ const MultiselectMenu = ({ className, title, options, value, onSelect }: Props)
|
|||
<div className={classNames(styles['multiselect-menu'], { [styles['active']]: menuOpen }, className)} ref={multiselectMenuRef}>
|
||||
<Button
|
||||
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
|
||||
disabled={disabled}
|
||||
onClick={toggleMenu}
|
||||
tabIndex={0}
|
||||
aria-haspopup='listbox'
|
||||
|
|
|
|||
2
src/components/MultiselectMenu/types.d.ts
vendored
2
src/components/MultiselectMenu/types.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
type MultiselectMenuOption = {
|
||||
id?: number;
|
||||
label: string;
|
||||
value: number;
|
||||
value: string | number | null;
|
||||
destination?: string;
|
||||
default?: boolean;
|
||||
hidden?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { Button } = require('stremio/components');
|
||||
const styles = require('./styles');
|
||||
|
||||
const Toggle = React.forwardRef(({ className, checked, children, ...props }, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={classnames(className, styles['toggle-container'], { 'checked': checked })}>
|
||||
<div className={styles['toggle']} />
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
Toggle.displayName = 'Toggle';
|
||||
|
||||
Toggle.propTypes = {
|
||||
className: PropTypes.string,
|
||||
checked: PropTypes.bool,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
module.exports = Toggle;
|
||||
27
src/components/Toggle/Toggle.tsx
Normal file
27
src/components/Toggle/Toggle.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
import React, { forwardRef } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Button } from 'stremio/components';
|
||||
import styles from './Toggle.less';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
checked: boolean,
|
||||
disabled?: boolean,
|
||||
tabIndex?: number,
|
||||
children?: React.ReactNode,
|
||||
};
|
||||
|
||||
const Toggle = forwardRef(({ className, checked, children, ...props }: Props, ref) => {
|
||||
return (
|
||||
<Button {...props} ref={ref} className={classnames(className, styles['toggle-container'], { 'checked': checked })}>
|
||||
<div className={styles['toggle']} />
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
Toggle.displayName = 'Toggle';
|
||||
|
||||
export default Toggle;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Toggle = require('./Toggle');
|
||||
|
||||
module.exports = Toggle;
|
||||
5
src/components/Toggle/index.ts
Normal file
5
src/components/Toggle/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
import Toggle from './Toggle';
|
||||
|
||||
export default Toggle;
|
||||
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
|
@ -3,4 +3,6 @@ declare module '*.less' {
|
|||
export = resource;
|
||||
}
|
||||
|
||||
declare module 'stremio-router';
|
||||
declare module 'stremio/components/NavBar';
|
||||
declare module 'stremio/components/ModalDialog';
|
||||
|
|
|
|||
23
src/routes/Player/Indicator/Indicator.less
Normal file
23
src/routes/Player/Indicator/Indicator.less
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.indicator-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 4rem;
|
||||
user-select: none;
|
||||
|
||||
.indicator {
|
||||
flex: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 2rem;
|
||||
border-radius: 4rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: var(--primary-foreground-color);
|
||||
background-color: var(--modal-background-color);
|
||||
}
|
||||
}
|
||||
73
src/routes/Player/Indicator/Indicator.tsx
Normal file
73
src/routes/Player/Indicator/Indicator.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import { Transition } from 'stremio/components';
|
||||
import { useBinaryState } from 'stremio/common';
|
||||
import styles from './Indicator.less';
|
||||
|
||||
type Property = {
|
||||
label: string,
|
||||
format: (value: number) => string,
|
||||
};
|
||||
|
||||
const PROPERTIES: Record<string, Property> = {
|
||||
'extraSubtitlesDelay': {
|
||||
label: 'SUBTITLES_DELAY',
|
||||
format: (value) => `${(value / 1000).toFixed(2)}s`,
|
||||
},
|
||||
};
|
||||
|
||||
type VideoState = Record<string, number>;
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
videoState: VideoState,
|
||||
};
|
||||
|
||||
const Indicator = ({ className, videoState }: Props) => {
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const prevVideoState = useRef<VideoState>(videoState);
|
||||
|
||||
const [shown, show, hide] = useBinaryState(false);
|
||||
const [current, setCurrent] = useState<string | null>(null);
|
||||
|
||||
const label = useMemo(() => {
|
||||
const property = current && PROPERTIES[current];
|
||||
return property && t(property.label);
|
||||
}, [current]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const property = current && PROPERTIES[current];
|
||||
const value = current && videoState[current];
|
||||
return property && value && property.format(value);
|
||||
}, [current, videoState]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const property of Object.keys(PROPERTIES)) {
|
||||
const prev = prevVideoState.current[property];
|
||||
const next = videoState[property];
|
||||
|
||||
if (next && next !== prev) {
|
||||
setCurrent(property);
|
||||
show();
|
||||
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
timeout.current = setTimeout(hide, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
prevVideoState.current = videoState;
|
||||
}, [videoState]);
|
||||
|
||||
return (
|
||||
<Transition when={shown} name={'fade'}>
|
||||
<div className={classNames(className, styles['indicator-container'])}>
|
||||
<div className={styles['indicator']}>
|
||||
<div>{label} {value}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indicator;
|
||||
|
|
@ -28,6 +28,7 @@ const useStatistics = require('./useStatistics');
|
|||
const useVideo = require('./useVideo');
|
||||
const styles = require('./styles');
|
||||
const Video = require('./Video');
|
||||
const { default: Indicator } = require('./Indicator/Indicator');
|
||||
|
||||
const Player = () => {
|
||||
const urlParams = useParams();
|
||||
|
|
@ -220,6 +221,16 @@ const Player = () => {
|
|||
video.setProp('extraSubtitlesDelay', delay);
|
||||
}, []);
|
||||
|
||||
const onIncreaseSubtitlesDelay = React.useCallback(() => {
|
||||
const delay = video.state.extraSubtitlesDelay + 250;
|
||||
onExtraSubtitlesDelayChanged(delay);
|
||||
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
||||
|
||||
const onDecreaseSubtitlesDelay = React.useCallback(() => {
|
||||
const delay = video.state.extraSubtitlesDelay - 250;
|
||||
onExtraSubtitlesDelayChanged(delay);
|
||||
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
||||
|
||||
const onSubtitlesSizeChanged = React.useCallback((size) => {
|
||||
updateSettings({ subtitlesSize: size });
|
||||
}, [updateSettings]);
|
||||
|
|
@ -591,6 +602,14 @@ const Player = () => {
|
|||
|
||||
break;
|
||||
}
|
||||
case 'KeyG': {
|
||||
onDecreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'KeyH': {
|
||||
onIncreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeMenus();
|
||||
!settings.escExitFullscreen && navigate(-1);
|
||||
|
|
@ -624,7 +643,29 @@ const Player = () => {
|
|||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('wheel', onWheel);
|
||||
};
|
||||
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, settings.escExitFullscreen, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]);
|
||||
}, [
|
||||
player.metaItem,
|
||||
player.selected,
|
||||
streamingServer.statistics,
|
||||
settings.seekTimeDuration,
|
||||
settings.seekShortTimeDuration,
|
||||
settings.escExitFullscreen,
|
||||
routeFocused,
|
||||
menusOpen,
|
||||
nextVideoPopupOpen,
|
||||
video.state.paused,
|
||||
video.state.time,
|
||||
video.state.volume,
|
||||
video.state.audioTracks,
|
||||
video.state.subtitlesTracks,
|
||||
video.state.extraSubtitlesTracks,
|
||||
video.state.playbackSpeed,
|
||||
toggleSubtitlesMenu,
|
||||
toggleStatisticsMenu,
|
||||
toggleSideDrawer,
|
||||
onDecreaseSubtitlesDelay,
|
||||
onIncreaseSubtitlesDelay,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
video.events.on('error', onError);
|
||||
|
|
@ -764,6 +805,10 @@ const Player = () => {
|
|||
onMouseOver={onBarMouseMove}
|
||||
onTouchEnd={onContainerMouseLeave}
|
||||
/>
|
||||
<Indicator
|
||||
className={classnames(styles['layer'], styles['indicator-layer'])}
|
||||
videoState={video.state}
|
||||
/>
|
||||
{
|
||||
nextVideoPopupOpen ?
|
||||
<NextVideoPopup
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ html:not(.active-slider-within) {
|
|||
}
|
||||
}
|
||||
|
||||
&.indicator-layer {
|
||||
top: initial;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 10rem;
|
||||
}
|
||||
|
||||
&.menu-layer {
|
||||
top: initial;
|
||||
left: initial;
|
||||
|
|
|
|||
11
src/routes/Settings/General/General.less
Normal file
11
src/routes/Settings/General/General.less
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
:import('~stremio/routes/Settings/components/Option/Option.less') {
|
||||
option-icon: icon;
|
||||
}
|
||||
|
||||
.trakt-container {
|
||||
margin-top: 2rem;
|
||||
|
||||
.option-icon {
|
||||
color: var(--color-trakt) !important;
|
||||
}
|
||||
}
|
||||
182
src/routes/Settings/General/General.tsx
Normal file
182
src/routes/Settings/General/General.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, MultiselectMenu, Toggle } from 'stremio/components';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { usePlatform, useToast } from 'stremio/common';
|
||||
import { Section, Option, Link } from '../components';
|
||||
import User from './User';
|
||||
import useDataExport from './useDataExport';
|
||||
import styles from './General.less';
|
||||
import useGeneralOptions from './useGeneralOptions';
|
||||
|
||||
type Props = {
|
||||
profile: Profile,
|
||||
};
|
||||
|
||||
const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { core, shell } = useServices();
|
||||
const platform = usePlatform();
|
||||
const toast = useToast();
|
||||
const [dataExport, loadDataExport] = useDataExport();
|
||||
|
||||
const {
|
||||
interfaceLanguageSelect,
|
||||
quitOnCloseToggle,
|
||||
escExitFullscreenToggle,
|
||||
hideSpoilersToggle,
|
||||
} = useGeneralOptions(profile);
|
||||
|
||||
const [traktAuthStarted, setTraktAuthStarted] = useState(false);
|
||||
|
||||
const isTraktAuthenticated = useMemo(() => {
|
||||
const trakt = profile?.auth?.user?.trakt;
|
||||
return trakt && (Date.now() / 1000) < (trakt.created_at + trakt.expires_in);
|
||||
}, [profile.auth]);
|
||||
|
||||
const onExportData = useCallback(() => {
|
||||
loadDataExport();
|
||||
}, []);
|
||||
|
||||
const onCalendarSubscribe = useCallback(() => {
|
||||
if (!profile.auth) return;
|
||||
|
||||
const protocol = platform.name === 'ios' ? 'webcal' : 'https';
|
||||
const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`;
|
||||
platform.openExternal(url);
|
||||
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: platform.name === 'ios' ?
|
||||
t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') :
|
||||
t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'),
|
||||
timeout: 25000
|
||||
});
|
||||
// Stremio 4 emits not documented event subscribeCalendar
|
||||
}, [profile.auth]);
|
||||
|
||||
const onToggleTrakt = useCallback(() => {
|
||||
if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') {
|
||||
platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`);
|
||||
setTraktAuthStarted(true);
|
||||
} else {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'LogoutTrakt'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isTraktAuthenticated, profile.auth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataExport.exportUrl) {
|
||||
platform.openExternal(dataExport.exportUrl);
|
||||
}
|
||||
}, [dataExport.exportUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTraktAuthenticated && traktAuthStarted) {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'InstallTraktAddon'
|
||||
}
|
||||
});
|
||||
setTraktAuthStarted(false);
|
||||
}
|
||||
}, [isTraktAuthenticated, traktAuthStarted]);
|
||||
|
||||
return <>
|
||||
<Section ref={ref}>
|
||||
<User profile={profile} />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
{
|
||||
profile?.auth?.user &&
|
||||
<Link
|
||||
label={t('SETTINGS_DATA_EXPORT')}
|
||||
onClick={onExportData}
|
||||
/>
|
||||
}
|
||||
{
|
||||
profile?.auth?.user &&
|
||||
<Link
|
||||
label={t('SETTINGS_SUBSCRIBE_CALENDAR')}
|
||||
onClick={onCalendarSubscribe}
|
||||
/>
|
||||
}
|
||||
<Link
|
||||
label={t('SETTINGS_SUPPORT')}
|
||||
href={'https://stremio.zendesk.com/hc/en-us'}
|
||||
/>
|
||||
<Link
|
||||
label={t('SETTINGS_SOURCE_CODE')}
|
||||
href={`https://github.com/stremio/stremio-web/tree/${process.env.COMMIT_HASH}`}
|
||||
/>
|
||||
<Link
|
||||
label={t('TERMS_OF_SERVICE')}
|
||||
href={'https://www.stremio.com/tos'}
|
||||
/>
|
||||
<Link
|
||||
label={t('PRIVACY_POLICY')}
|
||||
href={'https://www.stremio.com/privacy'}
|
||||
/>
|
||||
{
|
||||
profile?.auth?.user &&
|
||||
<Link
|
||||
label={t('SETTINGS_ACC_DELETE')}
|
||||
href={'https://stremio.zendesk.com/hc/en-us/articles/360021428911-How-to-delete-my-account'}
|
||||
/>
|
||||
}
|
||||
{
|
||||
profile?.auth?.user?.email &&
|
||||
<Link
|
||||
label={t('SETTINGS_CHANGE_PASSWORD')}
|
||||
href={`https://www.strem.io/reset-password/${profile.auth.user.email}`}
|
||||
/>
|
||||
}
|
||||
<Option className={styles['trakt-container']} icon={'trakt'} label={t('SETTINGS_TRAKT')}>
|
||||
<Button className={'button'} title={isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE')} disabled={profile.auth === null} tabIndex={-1} onClick={onToggleTrakt}>
|
||||
{isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE')}
|
||||
</Button>
|
||||
</Option>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Option label={'SETTINGS_UI_LANGUAGE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...interfaceLanguageSelect}
|
||||
/>
|
||||
</Option>
|
||||
{
|
||||
shell.active &&
|
||||
<Option label={'SETTINGS_QUIT_ON_CLOSE'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...quitOnCloseToggle}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
shell.active &&
|
||||
<Option label={'SETTINGS_FULLSCREEN_EXIT'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...escExitFullscreenToggle}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
<Option label={'SETTINGS_BLUR_UNWATCHED_IMAGE'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...hideSpoilersToggle}
|
||||
/>
|
||||
</Option>
|
||||
</Section>
|
||||
</>;
|
||||
});
|
||||
|
||||
export default General;
|
||||
87
src/routes/Settings/General/User/User.less
Normal file
87
src/routes/Settings/General/User/User.less
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
.user {
|
||||
gap: 1rem;
|
||||
|
||||
.user-info-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
margin-right: 1rem;
|
||||
border: 2px solid var(--primary-accent-color);
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-origin: content-box;
|
||||
background-clip: content-box;
|
||||
opacity: 0.9;
|
||||
background-color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.email-logout-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.email-label-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.email-label-container {
|
||||
.email-label {
|
||||
flex: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-panel-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 10rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 3.5rem;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.user-panel-label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
padding: 0 0.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.user {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.user-panel-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/routes/Settings/General/User/User.tsx
Normal file
66
src/routes/Settings/General/User/User.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { Link } from '../../components';
|
||||
import styles from './User.less';
|
||||
|
||||
type Props = {
|
||||
profile: Profile,
|
||||
};
|
||||
|
||||
const User = ({ profile }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { core } = useServices();
|
||||
|
||||
const avatar = useMemo(() => (
|
||||
!profile.auth ?
|
||||
`url('${require('/images/anonymous.png')}')`
|
||||
:
|
||||
profile.auth.user.avatar ?
|
||||
`url('${profile.auth.user.avatar}')`
|
||||
:
|
||||
`url('${require('/images/default_avatar.png')}')`
|
||||
), [profile.auth]);
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles['user']}>
|
||||
<div className={styles['user-info-content']}>
|
||||
<div
|
||||
className={styles['avatar-container']}
|
||||
style={{ backgroundImage: avatar }}
|
||||
/>
|
||||
<div className={styles['email-logout-container']}>
|
||||
<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 ? t('ANONYMOUS_USER') : profile.auth.user.email}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<Link
|
||||
label={t('LOG_OUT')}
|
||||
onClick={onLogout}
|
||||
/>
|
||||
:
|
||||
<Link
|
||||
label={`${t('LOG_IN')} / ${t('SIGN_UP')}`}
|
||||
href={'#/intro'}
|
||||
target={'_self'}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
2
src/routes/Settings/General/User/index.ts
Normal file
2
src/routes/Settings/General/User/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import User from './User';
|
||||
export default User;
|
||||
2
src/routes/Settings/General/index.ts
Normal file
2
src/routes/Settings/General/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import General from './General';
|
||||
export default General;
|
||||
6
src/routes/Settings/General/useDataExport.d.ts
vendored
Normal file
6
src/routes/Settings/General/useDataExport.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
declare const useDataExport: () => [
|
||||
DataExport,
|
||||
() => void,
|
||||
];
|
||||
|
||||
export = useDataExport;
|
||||
84
src/routes/Settings/General/useGeneralOptions.ts
Normal file
84
src/routes/Settings/General/useGeneralOptions.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useMemo } from 'react';
|
||||
import { interfaceLanguages } from 'stremio/common';
|
||||
import { useServices } from 'stremio/services';
|
||||
|
||||
const useGeneralOptions = (profile: Profile) => {
|
||||
const { core } = useServices();
|
||||
|
||||
const interfaceLanguageSelect = useMemo(() => ({
|
||||
options: interfaceLanguages.map(({ name, codes }) => ({
|
||||
value: codes[0],
|
||||
label: name,
|
||||
})),
|
||||
value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage,
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
interfaceLanguage: value
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const escExitFullscreenToggle = useMemo(() => ({
|
||||
checked: profile.settings.escExitFullscreen,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
escExitFullscreen: !profile.settings.escExitFullscreen
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const quitOnCloseToggle = useMemo(() => ({
|
||||
checked: profile.settings.quitOnClose,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
quitOnClose: !profile.settings.quitOnClose
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const hideSpoilersToggle = useMemo(() => ({
|
||||
checked: profile.settings.hideSpoilers,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
hideSpoilers: !profile.settings.hideSpoilers
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
return {
|
||||
interfaceLanguageSelect,
|
||||
escExitFullscreenToggle,
|
||||
quitOnCloseToggle,
|
||||
hideSpoilersToggle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGeneralOptions;
|
||||
31
src/routes/Settings/Info/Info.less
Normal file
31
src/routes/Settings/Info/Info.less
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/routes/Settings/components/Option/Option.less') {
|
||||
option-content: content;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: none;
|
||||
|
||||
.option-content {
|
||||
color: var(--primary-foreground-color);
|
||||
overflow: hidden;
|
||||
|
||||
.label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @xsmall) {
|
||||
.info {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.info {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
52
src/routes/Settings/Info/Info.tsx
Normal file
52
src/routes/Settings/Info/Info.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { Option, Section } from '../components';
|
||||
import styles from './Info.less';
|
||||
|
||||
type Props = {
|
||||
streamingServer: StreamingServer,
|
||||
};
|
||||
|
||||
const Info = ({ streamingServer }: Props) => {
|
||||
const { shell } = useServices();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const settings = useMemo(() => (
|
||||
streamingServer?.settings?.type === 'Ready' ?
|
||||
streamingServer.settings.content as StreamingServerSettings : null
|
||||
), [streamingServer?.settings]);
|
||||
|
||||
return (
|
||||
<Section className={styles['info']}>
|
||||
<Option label={t('SETTINGS_APP_VERSION')}>
|
||||
<div className={styles['label']}>
|
||||
{process.env.VERSION}
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={t('SETTINGS_BUILD_VERSION')}>
|
||||
<div className={styles['label']}>
|
||||
{process.env.COMMIT_HASH}
|
||||
</div>
|
||||
</Option>
|
||||
{
|
||||
settings?.serverVersion &&
|
||||
<Option label={t('SETTINGS_SERVER_VERSION')}>
|
||||
<div className={styles['label']}>
|
||||
{settings.serverVersion}
|
||||
</div>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' &&
|
||||
<Option label={t('SETTINGS_SHELL_VERSION')}>
|
||||
<div className={styles['label']}>
|
||||
{shell.transport.props.shellVersion}
|
||||
</div>
|
||||
</Option>
|
||||
}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
2
src/routes/Settings/Info/index.ts
Normal file
2
src/routes/Settings/Info/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Info from './Info';
|
||||
export default Info;
|
||||
62
src/routes/Settings/Menu/Menu.less
Normal file
62
src/routes/Settings/Menu/Menu.less
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
.menu {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 18rem;
|
||||
padding: 3rem 1.5rem;
|
||||
|
||||
.button {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
border-radius: 4rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.4;
|
||||
|
||||
&.selected {
|
||||
font-weight: 600;
|
||||
color: var(--primary-foreground-color);
|
||||
background-color: var(--overlay-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
}
|
||||
|
||||
.spacing {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-info-label {
|
||||
flex: 0 1 auto;
|
||||
margin: 0.5rem 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.3;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @xsmall) {
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
62
src/routes/Settings/Menu/Menu.tsx
Normal file
62
src/routes/Settings/Menu/Menu.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { Button } from 'stremio/components';
|
||||
import { SECTIONS } from '../constants';
|
||||
import styles from './Menu.less';
|
||||
|
||||
type Props = {
|
||||
selected: string,
|
||||
streamingServer: StreamingServer,
|
||||
onSelect: (event: React.MouseEvent<HTMLDivElement>) => void,
|
||||
};
|
||||
|
||||
const Menu = ({ selected, streamingServer, onSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { shell } = useServices();
|
||||
|
||||
const settings = useMemo(() => (
|
||||
streamingServer?.settings?.type === 'Ready' ?
|
||||
streamingServer.settings.content as StreamingServerSettings : null
|
||||
), [streamingServer?.settings]);
|
||||
|
||||
return (
|
||||
<div className={styles['menu']}>
|
||||
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.GENERAL })} title={t('SETTINGS_NAV_GENERAL')} data-section={SECTIONS.GENERAL} onClick={onSelect}>
|
||||
{ t('SETTINGS_NAV_GENERAL') }
|
||||
</Button>
|
||||
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.PLAYER })} title={t('SETTINGS_NAV_PLAYER')} data-section={SECTIONS.PLAYER} onClick={onSelect}>
|
||||
{ t('SETTINGS_NAV_PLAYER') }
|
||||
</Button>
|
||||
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.STREAMING })} title={t('SETTINGS_NAV_STREAMING')} data-section={SECTIONS.STREAMING} onClick={onSelect}>
|
||||
{ t('SETTINGS_NAV_STREAMING') }
|
||||
</Button>
|
||||
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.SHORTCUTS })} title={t('SETTINGS_NAV_SHORTCUTS')} data-section={SECTIONS.SHORTCUTS} onClick={onSelect}>
|
||||
{ t('SETTINGS_NAV_SHORTCUTS') }
|
||||
</Button>
|
||||
|
||||
<div className={styles['spacing']} />
|
||||
<div className={styles['version-info-label']} title={process.env.VERSION}>
|
||||
{t('SETTINGS_APP_VERSION')}: {process.env.VERSION}
|
||||
</div>
|
||||
<div className={styles['version-info-label']} title={process.env.COMMIT_HASH}>
|
||||
{t('SETTINGS_BUILD_VERSION')}: {process.env.COMMIT_HASH}
|
||||
</div>
|
||||
{
|
||||
settings?.serverVersion &&
|
||||
<div className={styles['version-info-label']} title={settings.serverVersion}>
|
||||
{t('SETTINGS_SERVER_VERSION')}: {settings.serverVersion}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' &&
|
||||
<div className={styles['version-info-label']} title={shell.transport.props.shellVersion}>
|
||||
{t('SETTINGS_SHELL_VERSION')}: {shell.transport.props.shellVersion}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
2
src/routes/Settings/Menu/index.ts
Normal file
2
src/routes/Settings/Menu/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Menu from './Menu';
|
||||
export default Menu;
|
||||
146
src/routes/Settings/Player/Player.tsx
Normal file
146
src/routes/Settings/Player/Player.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { ColorInput, MultiselectMenu, Toggle } from 'stremio/components';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { Category, Option, Section } from '../components';
|
||||
import usePlayerOptions from './usePlayerOptions';
|
||||
|
||||
type Props = {
|
||||
profile: Profile,
|
||||
};
|
||||
|
||||
const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
||||
const { shell } = useServices();
|
||||
|
||||
const {
|
||||
subtitlesLanguageSelect,
|
||||
subtitlesSizeSelect,
|
||||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
audioLanguageSelect,
|
||||
surroundSoundToggle,
|
||||
seekTimeDurationSelect,
|
||||
seekShortTimeDurationSelect,
|
||||
playInExternalPlayerSelect,
|
||||
nextVideoPopupDurationSelect,
|
||||
bingeWatchingToggle,
|
||||
playInBackgroundToggle,
|
||||
hardwareDecodingToggle,
|
||||
pauseOnMinimizeToggle,
|
||||
} = usePlayerOptions(profile);
|
||||
|
||||
return (
|
||||
<Section ref={ref} label={'SETTINGS_NAV_PLAYER'}>
|
||||
<Category icon={'subtitles'} label={'SETTINGS_SECTION_SUBTITLES'}>
|
||||
<Option label={'SETTINGS_SUBTITLES_LANGUAGE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...subtitlesLanguageSelect}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SUBTITLES_SIZE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...subtitlesSizeSelect}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SUBTITLES_COLOR'}>
|
||||
<ColorInput
|
||||
className={'color-input'}
|
||||
{...subtitlesTextColorInput}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SUBTITLES_COLOR_BACKGROUND'}>
|
||||
<ColorInput
|
||||
className={'color-input'}
|
||||
{...subtitlesBackgroundColorInput}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SUBTITLES_COLOR_OUTLINE'}>
|
||||
<ColorInput
|
||||
className={'color-input'}
|
||||
{...subtitlesOutlineColorInput}
|
||||
/>
|
||||
</Option>
|
||||
</Category>
|
||||
<Category icon={'volume-medium'} label={'SETTINGS_SECTION_AUDIO'}>
|
||||
<Option label={'SETTINGS_DEFAULT_AUDIO_TRACK'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...audioLanguageSelect}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SURROUND_SOUND'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...surroundSoundToggle}
|
||||
/>
|
||||
</Option>
|
||||
</Category>
|
||||
<Category icon={'remote'} label={'SETTINGS_SECTION_CONTROLS'}>
|
||||
<Option label={'SETTINGS_SEEK_KEY'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...seekTimeDurationSelect}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SEEK_KEY_SHIFT'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...seekShortTimeDurationSelect}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_PLAY_IN_BACKGROUND'}>
|
||||
<Toggle
|
||||
disabled={true}
|
||||
tabIndex={-1}
|
||||
{...playInBackgroundToggle}
|
||||
/>
|
||||
</Option>
|
||||
</Category>
|
||||
<Category icon={'play'} label={'SETTINGS_SECTION_AUTO_PLAY'}>
|
||||
<Option label={'AUTO_PLAY'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...bingeWatchingToggle}
|
||||
/>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_NEXT_VIDEO_POPUP_DURATION'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
disabled={!profile.settings.bingeWatching}
|
||||
{...nextVideoPopupDurationSelect}
|
||||
/>
|
||||
</Option>
|
||||
</Category>
|
||||
<Category icon={'glasses'} label={'SETTINGS_SECTION_ADVANCED'}>
|
||||
<Option label={'SETTINGS_PLAY_IN_EXTERNAL_PLAYER'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...playInExternalPlayerSelect}
|
||||
/>
|
||||
</Option>
|
||||
{
|
||||
shell.active &&
|
||||
<Option label={'SETTINGS_HWDEC'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...hardwareDecodingToggle}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
shell.active &&
|
||||
<Option label={'SETTINGS_PAUSE_MINIMIZED'}>
|
||||
<Toggle
|
||||
tabIndex={-1}
|
||||
{...pauseOnMinimizeToggle}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
</Category>
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
||||
export default Player;
|
||||
2
src/routes/Settings/Player/index.ts
Normal file
2
src/routes/Settings/Player/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Player from './Player';
|
||||
export default Player;
|
||||
|
|
@ -1,77 +1,25 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useServices } from 'stremio/services';
|
||||
import { CONSTANTS, languageNames, usePlatform } from 'stremio/common';
|
||||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { CONSTANTS, usePlatform, interfaceLanguages, languageNames } = require('stremio/common');
|
||||
const LANGUAGES_NAMES: Record<string, string> = languageNames;
|
||||
|
||||
const useProfileSettingsInputs = (profile) => {
|
||||
const usePlayerOptions = (profile: Profile) => {
|
||||
const { t } = useTranslation();
|
||||
const { core } = useServices();
|
||||
const platform = usePlatform();
|
||||
// TODO combine those useMemo in one
|
||||
const interfaceLanguageSelect = React.useMemo(() => ({
|
||||
options: interfaceLanguages.map(({ name, codes }) => ({
|
||||
value: codes[0],
|
||||
label: name,
|
||||
})),
|
||||
value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage,
|
||||
onSelect: (value) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
interfaceLanguage: value
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const hideSpoilersToggle = React.useMemo(() => ({
|
||||
checked: profile.settings.hideSpoilers,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
hideSpoilers: !profile.settings.hideSpoilers
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const quitOnCloseToggle = React.useMemo(() => ({
|
||||
checked: profile.settings.quitOnClose,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
quitOnClose: !profile.settings.quitOnClose
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const subtitlesLanguageSelect = React.useMemo(() => ({
|
||||
const subtitlesLanguageSelect = useMemo(() => ({
|
||||
options: [
|
||||
{ value: null, label: t('NONE') },
|
||||
...Object.keys(languageNames).map((code) => ({
|
||||
...Object.keys(LANGUAGES_NAMES).map((code) => ({
|
||||
value: code,
|
||||
label: languageNames[code]
|
||||
label: LANGUAGES_NAMES[code]
|
||||
}))
|
||||
],
|
||||
value: profile.settings.subtitlesLanguage,
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -84,7 +32,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesSizeSelect = React.useMemo(() => ({
|
||||
|
||||
const subtitlesSizeSelect = useMemo(() => ({
|
||||
options: CONSTANTS.SUBTITLES_SIZES.map((size) => ({
|
||||
value: `${size}`,
|
||||
label: `${size}%`
|
||||
|
|
@ -93,7 +42,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
title: () => {
|
||||
return `${profile.settings.subtitlesSize}%`;
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -106,9 +55,10 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesTextColorInput = React.useMemo(() => ({
|
||||
|
||||
const subtitlesTextColorInput = useMemo(() => ({
|
||||
value: profile.settings.subtitlesTextColor,
|
||||
onChange: (value) => {
|
||||
onChange: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -121,9 +71,10 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesBackgroundColorInput = React.useMemo(() => ({
|
||||
|
||||
const subtitlesBackgroundColorInput = useMemo(() => ({
|
||||
value: profile.settings.subtitlesBackgroundColor,
|
||||
onChange: (value) => {
|
||||
onChange: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -136,9 +87,10 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const subtitlesOutlineColorInput = React.useMemo(() => ({
|
||||
|
||||
const subtitlesOutlineColorInput = useMemo(() => ({
|
||||
value: profile.settings.subtitlesOutlineColor,
|
||||
onChange: (value) => {
|
||||
onChange: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -151,13 +103,14 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const audioLanguageSelect = React.useMemo(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
|
||||
const audioLanguageSelect = useMemo(() => ({
|
||||
options: Object.keys(LANGUAGES_NAMES).map((code) => ({
|
||||
value: code,
|
||||
label: languageNames[code]
|
||||
label: LANGUAGES_NAMES [code]
|
||||
})),
|
||||
value: profile.settings.audioLanguage,
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -170,7 +123,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const surroundSoundToggle = React.useMemo(() => ({
|
||||
|
||||
const surroundSoundToggle = useMemo(() => ({
|
||||
checked: profile.settings.surroundSound,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -185,23 +139,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const escExitFullscreenToggle = React.useMemo(() => ({
|
||||
checked: profile.settings.escExitFullscreen,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
escExitFullscreen: !profile.settings.escExitFullscreen
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
const seekTimeDurationSelect = React.useMemo(() => ({
|
||||
const seekTimeDurationSelect = useMemo(() => ({
|
||||
options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({
|
||||
value: `${size}`,
|
||||
label: `${size / 1000} ${t('SECONDS')}`
|
||||
|
|
@ -210,7 +149,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
title: () => {
|
||||
return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`;
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -223,7 +162,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const seekShortTimeDurationSelect = React.useMemo(() => ({
|
||||
|
||||
const seekShortTimeDurationSelect = useMemo(() => ({
|
||||
options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({
|
||||
value: `${size}`,
|
||||
label: `${size / 1000} ${t('SECONDS')}`
|
||||
|
|
@ -232,7 +172,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
title: () => {
|
||||
return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`;
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -245,7 +185,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const playInExternalPlayerSelect = React.useMemo(() => ({
|
||||
|
||||
const playInExternalPlayerSelect = useMemo(() => ({
|
||||
options: CONSTANTS.EXTERNAL_PLAYERS
|
||||
.filter(({ platforms }) => platforms.includes(platform.name))
|
||||
.map(({ label, value }) => ({
|
||||
|
|
@ -257,7 +198,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType);
|
||||
return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType;
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -270,7 +211,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const nextVideoPopupDurationSelect = React.useMemo(() => ({
|
||||
|
||||
const nextVideoPopupDurationSelect = useMemo(() => ({
|
||||
options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({
|
||||
value: `${duration}`,
|
||||
label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}`
|
||||
|
|
@ -282,7 +224,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
:
|
||||
`${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`;
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: string) => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
|
|
@ -295,7 +237,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const bingeWatchingToggle = React.useMemo(() => ({
|
||||
|
||||
const bingeWatchingToggle = useMemo(() => ({
|
||||
checked: profile.settings.bingeWatching,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -310,7 +253,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const playInBackgroundToggle = React.useMemo(() => ({
|
||||
|
||||
const playInBackgroundToggle = useMemo(() => ({
|
||||
checked: profile.settings.playInBackground,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -325,7 +269,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const hardwareDecodingToggle = React.useMemo(() => ({
|
||||
|
||||
const hardwareDecodingToggle = useMemo(() => ({
|
||||
checked: profile.settings.hardwareDecoding,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -340,7 +285,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
const pauseOnMinimizeToggle = React.useMemo(() => ({
|
||||
|
||||
const pauseOnMinimizeToggle = useMemo(() => ({
|
||||
checked: profile.settings.pauseOnMinimize,
|
||||
onClick: () => {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -355,9 +301,8 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [profile.settings]);
|
||||
|
||||
return {
|
||||
interfaceLanguageSelect,
|
||||
hideSpoilersToggle,
|
||||
subtitlesLanguageSelect,
|
||||
subtitlesSizeSelect,
|
||||
subtitlesTextColorInput,
|
||||
|
|
@ -365,8 +310,6 @@ const useProfileSettingsInputs = (profile) => {
|
|||
subtitlesOutlineColorInput,
|
||||
audioLanguageSelect,
|
||||
surroundSoundToggle,
|
||||
escExitFullscreenToggle,
|
||||
quitOnCloseToggle,
|
||||
seekTimeDurationSelect,
|
||||
seekShortTimeDurationSelect,
|
||||
playInExternalPlayerSelect,
|
||||
|
|
@ -378,4 +321,4 @@ const useProfileSettingsInputs = (profile) => {
|
|||
};
|
||||
};
|
||||
|
||||
module.exports = useProfileSettingsInputs;
|
||||
export default usePlayerOptions;
|
||||
|
|
@ -1,809 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const classnames = require('classnames');
|
||||
const throttle = require('lodash.throttle');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { default: useRouteFocused } = require('stremio/common/useRouteFocused');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common');
|
||||
const { Button, ColorInput, MainNavBars, MultiselectMenu, Toggle } = require('stremio/components');
|
||||
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
|
||||
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
|
||||
const useDataExport = require('./useDataExport');
|
||||
const styles = require('./styles');
|
||||
const { default: URLsManager } = require('./URLsManager/URLsManager');
|
||||
|
||||
const GENERAL_SECTION = 'general';
|
||||
const PLAYER_SECTION = 'player';
|
||||
const STREAMING_SECTION = 'streaming';
|
||||
const SHORTCUTS_SECTION = 'shortcuts';
|
||||
|
||||
const Settings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { core, shell } = useServices();
|
||||
const routeFocused = useRouteFocused();
|
||||
const profile = useProfile();
|
||||
const [dataExport, loadDataExport] = useDataExport();
|
||||
const streamingServer = useStreamingServer();
|
||||
const platform = usePlatform();
|
||||
const toast = useToast();
|
||||
const {
|
||||
interfaceLanguageSelect,
|
||||
hideSpoilersToggle,
|
||||
subtitlesLanguageSelect,
|
||||
subtitlesSizeSelect,
|
||||
subtitlesTextColorInput,
|
||||
subtitlesBackgroundColorInput,
|
||||
subtitlesOutlineColorInput,
|
||||
audioLanguageSelect,
|
||||
surroundSoundToggle,
|
||||
seekTimeDurationSelect,
|
||||
seekShortTimeDurationSelect,
|
||||
escExitFullscreenToggle,
|
||||
quitOnCloseToggle,
|
||||
playInExternalPlayerSelect,
|
||||
nextVideoPopupDurationSelect,
|
||||
bingeWatchingToggle,
|
||||
playInBackgroundToggle,
|
||||
hardwareDecodingToggle,
|
||||
pauseOnMinimizeToggle,
|
||||
} = useProfileSettingsInputs(profile);
|
||||
const {
|
||||
streamingServerRemoteUrlInput,
|
||||
remoteEndpointSelect,
|
||||
cacheSizeSelect,
|
||||
torrentProfileSelect,
|
||||
transcodingProfileSelect,
|
||||
} = useStreamingServerSettingsInputs(streamingServer);
|
||||
const [traktAuthStarted, setTraktAuthStarted] = React.useState(false);
|
||||
const isTraktAuthenticated = React.useMemo(() => {
|
||||
return profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null &&
|
||||
(Date.now() / 1000) < (profile.auth.user.trakt.created_at + profile.auth.user.trakt.expires_in);
|
||||
}, [profile.auth]);
|
||||
const logoutButtonOnClick = React.useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const toggleTraktOnClick = React.useCallback(() => {
|
||||
if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') {
|
||||
platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`);
|
||||
setTraktAuthStarted(true);
|
||||
} else {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'LogoutTrakt'
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isTraktAuthenticated, profile.auth]);
|
||||
const subscribeCalendarOnClick = React.useCallback(() => {
|
||||
if (!profile.auth) return;
|
||||
|
||||
const protocol = platform.name === 'ios' ? 'webcal' : 'https';
|
||||
const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`;
|
||||
platform.openExternal(url);
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: platform.name === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'),
|
||||
timeout: 25000
|
||||
});
|
||||
// Stremio 4 emits not documented event subscribeCalendar
|
||||
}, [profile.auth]);
|
||||
const exportDataOnClick = React.useCallback(() => {
|
||||
loadDataExport();
|
||||
}, []);
|
||||
const onCopyRemoteUrlClick = React.useCallback(() => {
|
||||
if (streamingServer.remoteUrl) {
|
||||
navigator.clipboard.writeText(streamingServer.remoteUrl);
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: t('SETTINGS_REMOTE_URL_COPIED'),
|
||||
timeout: 2500,
|
||||
});
|
||||
}
|
||||
}, [streamingServer.remoteUrl]);
|
||||
const sectionsContainerRef = React.useRef(null);
|
||||
const generalSectionRef = React.useRef(null);
|
||||
const playerSectionRef = React.useRef(null);
|
||||
const streamingServerSectionRef = React.useRef(null);
|
||||
const shortcutsSectionRef = React.useRef(null);
|
||||
const sections = React.useMemo(() => ([
|
||||
{ ref: generalSectionRef, id: GENERAL_SECTION },
|
||||
{ ref: playerSectionRef, id: PLAYER_SECTION },
|
||||
{ ref: streamingServerSectionRef, id: STREAMING_SECTION },
|
||||
{ ref: shortcutsSectionRef, id: SHORTCUTS_SECTION },
|
||||
]), []);
|
||||
const [selectedSectionId, setSelectedSectionId] = React.useState(GENERAL_SECTION);
|
||||
const updateSelectedSectionId = React.useCallback(() => {
|
||||
if (sectionsContainerRef.current.scrollTop + sectionsContainerRef.current.clientHeight >= sectionsContainerRef.current.scrollHeight - 50) {
|
||||
setSelectedSectionId(sections[sections.length - 1].id);
|
||||
} else {
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].ref.current.offsetTop - sectionsContainerRef.current.offsetTop <= sectionsContainerRef.current.scrollTop) {
|
||||
setSelectedSectionId(sections[i].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
const sideMenuButtonOnClick = React.useCallback((event) => {
|
||||
const section = sections.find((section) => {
|
||||
return section.id === event.currentTarget.dataset.section;
|
||||
});
|
||||
sectionsContainerRef.current.scrollTo({
|
||||
top: section.ref.current.offsetTop - sectionsContainerRef.current.offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, []);
|
||||
const sectionsContainerOnScroll = React.useCallback(throttle(() => {
|
||||
updateSelectedSectionId();
|
||||
}, 50), []);
|
||||
React.useEffect(() => {
|
||||
if (isTraktAuthenticated && traktAuthStarted) {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'InstallTraktAddon'
|
||||
}
|
||||
});
|
||||
setTraktAuthStarted(false);
|
||||
}
|
||||
}, [isTraktAuthenticated, traktAuthStarted]);
|
||||
React.useEffect(() => {
|
||||
if (dataExport.exportUrl !== null && typeof dataExport.exportUrl === 'string') {
|
||||
platform.openExternal(dataExport.exportUrl);
|
||||
}
|
||||
}, [dataExport.exportUrl]);
|
||||
React.useLayoutEffect(() => {
|
||||
if (routeFocused) {
|
||||
updateSelectedSectionId();
|
||||
}
|
||||
}, [routeFocused]);
|
||||
return (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'}>
|
||||
<div className={classnames(styles['settings-content'], 'animation-fade-in')}>
|
||||
<div className={styles['side-menu-container']}>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === GENERAL_SECTION })} title={ t('SETTINGS_NAV_GENERAL') } data-section={GENERAL_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
{ t('SETTINGS_NAV_GENERAL') }
|
||||
</Button>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === PLAYER_SECTION })} title={ t('SETTINGS_NAV_PLAYER') }data-section={PLAYER_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
{ t('SETTINGS_NAV_PLAYER') }
|
||||
</Button>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === STREAMING_SECTION })} title={ t('SETTINGS_NAV_STREAMING') } data-section={STREAMING_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
{ t('SETTINGS_NAV_STREAMING') }
|
||||
</Button>
|
||||
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === SHORTCUTS_SECTION })} title={ t('SETTINGS_NAV_SHORTCUTS') } data-section={SHORTCUTS_SECTION} onClick={sideMenuButtonOnClick}>
|
||||
{ t('SETTINGS_NAV_SHORTCUTS') }
|
||||
</Button>
|
||||
<div className={styles['spacing']} />
|
||||
<div className={styles['version-info-label']} title={process.env.VERSION}>
|
||||
{`${t('SETTINGS_APP_VERSION')}: ${process.env.VERSION}`}
|
||||
</div>
|
||||
<div className={styles['version-info-label']} title={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}>{`${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}>{`${t('SETTINGS_APP_VERSION')}: ${shell.transport.props.shellVersion}`}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div ref={sectionsContainerRef} className={styles['sections-container']} onScroll={sectionsContainerOnScroll}>
|
||||
<div ref={generalSectionRef} className={styles['section-container']}>
|
||||
<div className={classnames(styles['option-container'], styles['user-info-option-container'])}>
|
||||
<div className={styles['user-info-content']}>
|
||||
<div
|
||||
className={styles['avatar-container']}
|
||||
style={{
|
||||
backgroundImage: profile.auth === null ?
|
||||
`url('${require('/images/anonymous.png')}')`
|
||||
:
|
||||
profile.auth.user.avatar ?
|
||||
`url('${profile.auth.user.avatar}')`
|
||||
:
|
||||
`url('${require('/images/default_avatar.png')}')`
|
||||
}}
|
||||
/>
|
||||
<div className={styles['email-logout-container']}>
|
||||
<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 ? t('ANONYMOUS_USER') : profile.auth.user.email}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<Button className={styles['logout-button-container']} title={ t('LOG_OUT') } onClick={logoutButtonOnClick}>
|
||||
<div className={styles['logout-label']}>{ t('LOG_OUT') }</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
profile.auth === null ?
|
||||
<div className={styles['option-container']}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={`${t('LOG_IN')} / ${t('SIGN_UP')}`} href={'#/intro'}>
|
||||
<div className={styles['label']}>{ t('LOG_IN') } / { t('SIGN_UP') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
{
|
||||
profile.auth ?
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_DATA_EXPORT')} tabIndex={-1} onClick={exportDataOnClick}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_DATA_EXPORT') }</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_SUBSCRIBE_CALENDAR')} tabIndex={-1} onClick={subscribeCalendarOnClick}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBSCRIBE_CALENDAR') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_SUPPORT')} target={'_blank'} href={'https://stremio.zendesk.com/hc/en-us'}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUPPORT') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<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'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('TERMS_OF_SERVICE')} target={'_blank'} href={'https://www.stremio.com/tos'}>
|
||||
<div className={styles['label']}>{ t('TERMS_OF_SERVICE') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('PRIVACY_POLICY')} target={'_blank'} href={'https://www.stremio.com/privacy'}>
|
||||
<div className={styles['label']}>{ t('PRIVACY_POLICY') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
profile.auth !== null && profile.auth.user !== null ?
|
||||
<div className={classnames(styles['option-container'], styles['link-container'])}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_ACC_DELETE')} target={'_blank'} href={'https://stremio.zendesk.com/hc/en-us/articles/360021428911-How-to-delete-my-account'}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_ACC_DELETE') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user.email === 'string' ?
|
||||
<div className={styles['option-container']}>
|
||||
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_CHANGE_PASSWORD')} target={'_blank'} href={`https://www.strem.io/reset-password/${profile.auth.user.email}`}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_CHANGE_PASSWORD') }</div>
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<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']}>{t('SETTINGS_TRAKT')}</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}>
|
||||
<div className={styles['label']}>
|
||||
{ isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_UI_LANGUAGE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
tabIndex={-1}
|
||||
{...interfaceLanguageSelect}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
shell.active &&
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_QUIT_ON_CLOSE') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
tabIndex={-1}
|
||||
{...quitOnCloseToggle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
shell.active &&
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_FULLSCREEN_EXIT') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
{...escExitFullscreenToggle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_BLUR_UNWATCHED_IMAGE') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
tabIndex={-1}
|
||||
{...hideSpoilersToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={playerSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
||||
<div className={styles['section-category-container']}>
|
||||
<Icon className={styles['icon']} name={'subtitles'} />
|
||||
<div className={styles['label']}>{t('SETTINGS_SECTION_SUBTITLES')}</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_LANGUAGE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...subtitlesLanguageSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_SIZE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...subtitlesSizeSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR') }</div>
|
||||
</div>
|
||||
<ColorInput
|
||||
className={classnames(styles['option-input-container'], styles['color-input-container'])}
|
||||
{...subtitlesTextColorInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR_BACKGROUND') }</div>
|
||||
</div>
|
||||
<ColorInput
|
||||
className={classnames(styles['option-input-container'], styles['color-input-container'])}
|
||||
{...subtitlesBackgroundColorInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR_OUTLINE') }</div>
|
||||
</div>
|
||||
<ColorInput
|
||||
className={classnames(styles['option-input-container'], styles['color-input-container'])}
|
||||
{...subtitlesOutlineColorInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={styles['section-category-container']}>
|
||||
<Icon className={styles['icon']} name={'volume-medium'} />
|
||||
<div className={styles['label']}>{t('SETTINGS_SECTION_AUDIO')}</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...audioLanguageSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SURROUND_SOUND') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
tabIndex={-1}
|
||||
{...surroundSoundToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={styles['section-category-container']}>
|
||||
<Icon className={styles['icon']} name={'remote'} />
|
||||
<div className={styles['label']}>{t('SETTINGS_SECTION_CONTROLS')}</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...seekTimeDurationSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SEEK_KEY_SHIFT') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...seekShortTimeDurationSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_BACKGROUND') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
disabled={true}
|
||||
tabIndex={-1}
|
||||
{...playInBackgroundToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={styles['section-category-container']}>
|
||||
<Icon className={styles['icon']} name={'play'} />
|
||||
<div className={styles['label']}>{t('SETTINGS_SECTION_AUTO_PLAY')}</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('AUTO_PLAY') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
{...bingeWatchingToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
disabled={!profile.settings.bingeWatching}
|
||||
{...nextVideoPopupDurationSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['section-container']}>
|
||||
<div className={styles['section-category-container']}>
|
||||
<Icon className={styles['icon']} name={'glasses'} />
|
||||
<div className={styles['label']}>{t('SETTINGS_SECTION_ADVANCED')}</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...playInExternalPlayerSelect}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
shell.active &&
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
tabIndex={-1}
|
||||
{...hardwareDecodingToggle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
shell.active &&
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_PAUSE_MINIMIZED') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
{...pauseOnMinimizeToggle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div ref={streamingServerSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
|
||||
<URLsManager />
|
||||
{
|
||||
streamingServerRemoteUrlInput.value !== null ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{t('SETTINGS_REMOTE_URL')}</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['configure-input-container'])}>
|
||||
<div className={styles['label']} title={streamingServerRemoteUrlInput.value}>{streamingServerRemoteUrlInput.value}</div>
|
||||
<Button className={styles['configure-button-container']} title={t('SETTINGS_COPY_REMOTE_URL')} onClick={onCopyRemoteUrlClick}>
|
||||
<Icon className={styles['icon']} name={'link'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
profile.auth !== null && profile.auth.user !== null && remoteEndpointSelect !== null ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_HTTPS_ENDPOINT') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...remoteEndpointSelect}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
cacheSizeSelect !== null ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SERVER_CACHE_SIZE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...cacheSizeSelect}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
torrentProfileSelect !== null ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SERVER_TORRENT_PROFILE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...torrentProfileSelect}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
transcodingProfileSelect !== null ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_TRANSCODE_PROFILE') }</div>
|
||||
</div>
|
||||
<MultiselectMenu
|
||||
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
|
||||
{...transcodingProfileSelect}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div ref={shortcutsSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_SHORTCUTS') }</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_PLAY_PAUSE') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>{ t('SETTINGS_SHORTCUT_SPACE') }</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SEEK_FORWARD') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>→</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_OR') }</div>
|
||||
<kbd>⇧ { t('SETTINGS_SHORTCUT_SHIFT') }</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>→</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SEEK_BACKWARD') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>←</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_OR') }</div>
|
||||
<kbd>⇧ { t('SETTINGS_SHORTCUT_SHIFT') }</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>←</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_VOLUME_UP') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↑</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_VOLUME_DOWN') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>↓</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_SUBTITLES') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_AUDIO') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>A</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_INFO') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>I</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_VIDEOS') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>V</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_FULLSCREEN') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>1</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_TO') }</div>
|
||||
<kbd>6</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_GO_TO_SEARCH') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>0</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_EXIT_BACK') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>{ t('SETTINGS_SHORTCUT_ESC') }</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['section-container'], styles['versions-section-container'])}>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
{t('SETTINGS_APP_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
<div className={styles['label']}>
|
||||
{process.env.VERSION}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
{t('SETTINGS_BUILD_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
<div className={styles['label']}>
|
||||
{process.env.COMMIT_HASH}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
streamingServer.settings !== null && streamingServer.settings.type === 'Ready' ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
{t('SETTINGS_SERVER_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
<div className={styles['label']}>
|
||||
{streamingServer.settings.content.serverVersion}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
{t('SETTINGS_SHELL_VERSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
<div className={styles['label']}>
|
||||
{ shell.transport.props.shellVersion }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainNavBars>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsFallback = () => (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'} />
|
||||
);
|
||||
|
||||
module.exports = withCoreSuspender(Settings, SettingsFallback);
|
||||
35
src/routes/Settings/Settings.less
Normal file
35
src/routes/Settings/Settings.less
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
.settings-container {
|
||||
height: calc(100% - var(--safe-area-inset-bottom));
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
|
||||
.settings-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.sections-container {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
padding: 0 3rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.settings-container {
|
||||
.settings-content {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.sections-container {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/routes/Settings/Settings.tsx
Normal file
108
src/routes/Settings/Settings.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { useProfile, useStreamingServer, useRouteFocused, withCoreSuspender } from 'stremio/common';
|
||||
import { MainNavBars } from 'stremio/components';
|
||||
import { SECTIONS } from './constants';
|
||||
import Menu from './Menu';
|
||||
import General from './General';
|
||||
import Player from './Player';
|
||||
import Streaming from './Streaming';
|
||||
import Shortcuts from './Shortcuts';
|
||||
import Info from './Info';
|
||||
import styles from './Settings.less';
|
||||
|
||||
const Settings = () => {
|
||||
const routeFocused = useRouteFocused();
|
||||
const profile = useProfile();
|
||||
const streamingServer = useStreamingServer();
|
||||
|
||||
const sectionsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const generalSectionRef = useRef<HTMLDivElement>(null);
|
||||
const playerSectionRef = useRef<HTMLDivElement>(null);
|
||||
const streamingServerSectionRef = useRef<HTMLDivElement>(null);
|
||||
const shortcutsSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sections = useMemo(() => ([
|
||||
{ ref: generalSectionRef, id: SECTIONS.GENERAL },
|
||||
{ ref: playerSectionRef, id: SECTIONS.PLAYER },
|
||||
{ ref: streamingServerSectionRef, id: SECTIONS.STREAMING },
|
||||
{ ref: shortcutsSectionRef, id: SECTIONS.SHORTCUTS },
|
||||
]), []);
|
||||
|
||||
const [selectedSectionId, setSelectedSectionId] = useState(SECTIONS.GENERAL);
|
||||
|
||||
const updateSelectedSectionId = useCallback(() => {
|
||||
const container = sectionsContainerRef.current;
|
||||
if (container!.scrollTop + container!.clientHeight >= container!.scrollHeight - 50) {
|
||||
setSelectedSectionId(sections[sections.length - 1].id);
|
||||
} else {
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].ref.current!.offsetTop - container!.offsetTop <= container!.scrollTop) {
|
||||
setSelectedSectionId(sections[i].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onMenuSelect = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const section = sections.find((section) => {
|
||||
return section.id === event.currentTarget.dataset.section;
|
||||
});
|
||||
|
||||
const container = sectionsContainerRef.current;
|
||||
section && container!.scrollTo({
|
||||
top: section.ref.current!.offsetTop - container!.offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onContainerScroll = useCallback(throttle(() => {
|
||||
updateSelectedSectionId();
|
||||
}, 50), []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (routeFocused) {
|
||||
updateSelectedSectionId();
|
||||
}
|
||||
}, [routeFocused]);
|
||||
|
||||
return (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'}>
|
||||
<div className={classnames(styles['settings-content'], 'animation-fade-in')}>
|
||||
<Menu
|
||||
selected={selectedSectionId}
|
||||
streamingServer={streamingServer}
|
||||
onSelect={onMenuSelect}
|
||||
/>
|
||||
|
||||
<div ref={sectionsContainerRef} className={styles['sections-container']} onScroll={onContainerScroll}>
|
||||
<General
|
||||
ref={generalSectionRef}
|
||||
profile={profile}
|
||||
/>
|
||||
<Player
|
||||
ref={playerSectionRef}
|
||||
profile={profile}
|
||||
/>
|
||||
<Streaming
|
||||
ref={streamingServerSectionRef}
|
||||
profile={profile}
|
||||
streamingServer={streamingServer}
|
||||
/>
|
||||
<Shortcuts ref={shortcutsSectionRef} />
|
||||
<Info streamingServer={streamingServer} />
|
||||
</div>
|
||||
</div>
|
||||
</MainNavBars>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsFallback = () => (
|
||||
<MainNavBars className={styles['settings-container']} route={'settings'} />
|
||||
);
|
||||
|
||||
export default withCoreSuspender(Settings, SettingsFallback);
|
||||
27
src/routes/Settings/Shortcuts/Shortcuts.less
Normal file
27
src/routes/Settings/Shortcuts/Shortcuts.less
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.shortcut-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
|
||||
kbd {
|
||||
flex: 0 1 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
border-radius: 0.25em;
|
||||
box-shadow: 0 4px 0 1px var(--modal-background-color);
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
margin: 0 1rem;
|
||||
white-space: nowrap;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
97
src/routes/Settings/Shortcuts/Shortcuts.tsx
Normal file
97
src/routes/Settings/Shortcuts/Shortcuts.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Section, Option } from '../components';
|
||||
import styles from './Shortcuts.less';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Shortcuts = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Section ref={ref} label={'SETTINGS_NAV_SHORTCUTS'}>
|
||||
<Option label={'SETTINGS_SHORTCUT_PLAY_PAUSE'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>{t('SETTINGS_SHORTCUT_SPACE')}</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SEEK_FORWARD'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>→</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
|
||||
<kbd>⇧ {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>→</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SEEK_BACKWARD'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>←</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
|
||||
<kbd>⇧ {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
|
||||
<div className={styles['label']}>+</div>
|
||||
<kbd>←</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_VOLUME_UP'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>↑</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_VOLUME_DOWN'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>↓</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_SUBTITLES'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_AUDIO'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>A</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_INFO'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>I</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_MENU_VIDEOS'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>V</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>F</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_DELAY'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>G</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
|
||||
<kbd>H</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_NAVIGATE_MENUS'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>1</kbd>
|
||||
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_TO')}</div>
|
||||
<kbd>6</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_GO_TO_SEARCH'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>0</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
<Option label={'SETTINGS_SHORTCUT_EXIT_BACK'}>
|
||||
<div className={styles['shortcut-container']}>
|
||||
<kbd>{t('SETTINGS_SHORTCUT_ESC')}</kbd>
|
||||
</div>
|
||||
</Option>
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
||||
export default Shortcuts;
|
||||
2
src/routes/Settings/Shortcuts/index.ts
Normal file
2
src/routes/Settings/Shortcuts/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Shortcuts from './Shortcuts';
|
||||
export default Shortcuts;
|
||||
44
src/routes/Settings/Streaming/Streaming.less
Normal file
44
src/routes/Settings/Streaming/Streaming.less
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
:import('~stremio/routes/Settings/components/Option/Option.less') {
|
||||
option-content: content;
|
||||
}
|
||||
|
||||
.configure-input-container {
|
||||
.option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
|
||||
.label {
|
||||
flex: auto;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.configure-button-container {
|
||||
flex: none;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/routes/Settings/Streaming/Streaming.tsx
Normal file
92
src/routes/Settings/Streaming/Streaming.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React, { forwardRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import { Button, MultiselectMenu } from 'stremio/components';
|
||||
import { useToast } from 'stremio/common';
|
||||
import { Section, Option } from '../components';
|
||||
import URLsManager from './URLsManager';
|
||||
import useStreamingOptions from './useStreamingOptions';
|
||||
import styles from './Streaming.less';
|
||||
|
||||
type Props = {
|
||||
profile: Profile,
|
||||
streamingServer: StreamingServer,
|
||||
};
|
||||
|
||||
const Streaming = forwardRef<HTMLDivElement, Props>(({ profile, streamingServer }: Props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
const {
|
||||
streamingServerRemoteUrlInput,
|
||||
remoteEndpointSelect,
|
||||
cacheSizeSelect,
|
||||
torrentProfileSelect,
|
||||
transcodingProfileSelect,
|
||||
} = useStreamingOptions(streamingServer);
|
||||
|
||||
const onCopyRemoteUrl = useCallback(() => {
|
||||
if (streamingServer.remoteUrl) {
|
||||
navigator.clipboard.writeText(streamingServer.remoteUrl);
|
||||
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: t('SETTINGS_REMOTE_URL_COPIED'),
|
||||
timeout: 2500,
|
||||
});
|
||||
}
|
||||
}, [streamingServer.remoteUrl]);
|
||||
|
||||
return (
|
||||
<Section ref={ref} label={'SETTINGS_NAV_STREAMING'}>
|
||||
<URLsManager />
|
||||
{
|
||||
streamingServerRemoteUrlInput.value !== null &&
|
||||
<Option className={styles['configure-input-container']} label={'SETTINGS_REMOTE_URL'}>
|
||||
<div className={styles['label']} title={streamingServerRemoteUrlInput.value}>{streamingServerRemoteUrlInput.value}</div>
|
||||
<Button className={styles['configure-button-container']} title={t('SETTINGS_COPY_REMOTE_URL')} onClick={onCopyRemoteUrl}>
|
||||
<Icon className={styles['icon']} name={'link'} />
|
||||
</Button>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
profile.auth !== null && profile.auth.user !== null && remoteEndpointSelect !== null &&
|
||||
<Option label={'SETTINGS_HTTPS_ENDPOINT'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...remoteEndpointSelect}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
cacheSizeSelect !== null &&
|
||||
<Option label={'SETTINGS_SERVER_CACHE_SIZE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...cacheSizeSelect}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
torrentProfileSelect !== null &&
|
||||
<Option label={'SETTINGS_SERVER_TORRENT_PROFILE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...torrentProfileSelect}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
{
|
||||
transcodingProfileSelect !== null &&
|
||||
<Option label={'SETTINGS_TRANSCODE_PROFILE'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
{...transcodingProfileSelect}
|
||||
/>
|
||||
</Option>
|
||||
}
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
||||
export default Streaming;
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 35rem;
|
||||
2
src/routes/Settings/Streaming/index.ts
Normal file
2
src/routes/Settings/Streaming/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Streaming from './Streaming';
|
||||
export default Streaming;
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const isEqual = require('lodash.isequal');
|
||||
const { useServices } = require('stremio/services');
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { useServices } from 'stremio/services';
|
||||
|
||||
const CACHE_SIZES = [0, 2147483648, 5368709120, 10737418240, null];
|
||||
|
||||
const cacheSizeToString = (size) => {
|
||||
const cacheSizeToString = (size: number | null) => {
|
||||
return size === null ?
|
||||
'Infinite'
|
||||
:
|
||||
|
|
@ -17,7 +17,16 @@ const cacheSizeToString = (size) => {
|
|||
`${Math.ceil(((size / 1024 / 1024 / 1024) + Number.EPSILON) * 100) / 100}GiB`;
|
||||
};
|
||||
|
||||
const TORRENT_PROFILES = {
|
||||
type TorrentProfile = {
|
||||
btDownloadSpeedHardLimit: number,
|
||||
btDownloadSpeedSoftLimit: number,
|
||||
btHandshakeTimeout: number,
|
||||
btMaxConnections: number,
|
||||
btMinPeersForStable: number,
|
||||
btRequestTimeout: number
|
||||
};
|
||||
|
||||
const TORRENT_PROFILES: Record<string, TorrentProfile> = {
|
||||
default: {
|
||||
btDownloadSpeedHardLimit: 3670016,
|
||||
btDownloadSpeedSoftLimit: 2621440,
|
||||
|
|
@ -52,17 +61,32 @@ const TORRENT_PROFILES = {
|
|||
}
|
||||
};
|
||||
|
||||
const useStreamingServerSettingsInputs = (streamingServer) => {
|
||||
const useStreamingOptions = (streamingServer: StreamingServer) => {
|
||||
const { core } = useServices();
|
||||
const { t } = useTranslation();
|
||||
// TODO combine those useMemo in one
|
||||
|
||||
const streamingServerRemoteUrlInput = React.useMemo(() => ({
|
||||
const settings = useMemo(() => (
|
||||
streamingServer?.settings?.type === 'Ready' ?
|
||||
streamingServer.settings.content as StreamingServerSettings : null
|
||||
), [streamingServer.settings]);
|
||||
|
||||
const networkInfo = useMemo(() => (
|
||||
streamingServer?.networkInfo?.type === 'Ready' ?
|
||||
streamingServer.networkInfo.content as NetworkInfo : null
|
||||
), [streamingServer.networkInfo]);
|
||||
|
||||
const deviceInfo = useMemo(() => (
|
||||
streamingServer?.deviceInfo?.type === 'Ready' ?
|
||||
streamingServer.deviceInfo.content as DeviceInfo : null
|
||||
), [streamingServer.deviceInfo]);
|
||||
|
||||
const streamingServerRemoteUrlInput = useMemo(() => ({
|
||||
value: streamingServer.remoteUrl,
|
||||
}), [streamingServer.remoteUrl]);
|
||||
|
||||
const remoteEndpointSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings?.type !== 'Ready' || streamingServer.networkInfo?.type !== 'Ready') {
|
||||
const remoteEndpointSelect = useMemo(() => {
|
||||
if (!settings || !networkInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -72,29 +96,29 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
label: t('SETTINGS_DISABLED'),
|
||||
value: '',
|
||||
},
|
||||
...streamingServer.networkInfo.content.availableInterfaces.map((address) => ({
|
||||
...networkInfo.availableInterfaces.map((address) => ({
|
||||
label: address,
|
||||
value: address,
|
||||
}))
|
||||
],
|
||||
value: streamingServer.settings.content.remoteHttps,
|
||||
onSelect: (value) => {
|
||||
value: settings.remoteHttps,
|
||||
onSelect: (value: string | null) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...streamingServer.settings.content,
|
||||
...settings,
|
||||
remoteHttps: value,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [streamingServer.settings, streamingServer.networkInfo]);
|
||||
}, [settings, networkInfo]);
|
||||
|
||||
const cacheSizeSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') {
|
||||
const cacheSizeSelect = useMemo(() => {
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -103,36 +127,37 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
label: cacheSizeToString(size),
|
||||
value: JSON.stringify(size)
|
||||
})),
|
||||
value: JSON.stringify(streamingServer.settings.content.cacheSize),
|
||||
value: JSON.stringify(settings.cacheSize),
|
||||
title: () => {
|
||||
return cacheSizeToString(streamingServer.settings.content.cacheSize);
|
||||
return cacheSizeToString(settings.cacheSize);
|
||||
},
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: any) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...streamingServer.settings.content,
|
||||
...settings,
|
||||
cacheSize: JSON.parse(value),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [streamingServer.settings]);
|
||||
const torrentProfileSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings === null || streamingServer.settings.type !== 'Ready') {
|
||||
}, [settings]);
|
||||
|
||||
const torrentProfileSelect = useMemo(() => {
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedTorrentProfile = {
|
||||
btDownloadSpeedHardLimit: streamingServer.settings.content.btDownloadSpeedHardLimit,
|
||||
btDownloadSpeedSoftLimit: streamingServer.settings.content.btDownloadSpeedSoftLimit,
|
||||
btHandshakeTimeout: streamingServer.settings.content.btHandshakeTimeout,
|
||||
btMaxConnections: streamingServer.settings.content.btMaxConnections,
|
||||
btMinPeersForStable: streamingServer.settings.content.btMinPeersForStable,
|
||||
btRequestTimeout: streamingServer.settings.content.btRequestTimeout
|
||||
btDownloadSpeedHardLimit: settings.btDownloadSpeedHardLimit,
|
||||
btDownloadSpeedSoftLimit: settings.btDownloadSpeedSoftLimit,
|
||||
btHandshakeTimeout: settings.btHandshakeTimeout,
|
||||
btMaxConnections: settings.btMaxConnections,
|
||||
btMinPeersForStable: settings.btMinPeersForStable,
|
||||
btRequestTimeout: settings.btRequestTimeout
|
||||
};
|
||||
const isCustomTorrentProfileSelected = Object.values(TORRENT_PROFILES).every((torrentProfile) => {
|
||||
return !isEqual(torrentProfile, selectedTorrentProfile);
|
||||
|
|
@ -153,22 +178,23 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
[]
|
||||
),
|
||||
value: JSON.stringify(selectedTorrentProfile),
|
||||
onSelect: (value) => {
|
||||
onSelect: (value: any) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...streamingServer.settings.content,
|
||||
...settings,
|
||||
...JSON.parse(value),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [streamingServer.settings]);
|
||||
const transcodingProfileSelect = React.useMemo(() => {
|
||||
if (streamingServer.settings?.type !== 'Ready' || streamingServer.deviceInfo?.type !== 'Ready') {
|
||||
}, [settings]);
|
||||
|
||||
const transcodingProfileSelect = useMemo(() => {
|
||||
if (!settings || !deviceInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -178,27 +204,34 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
label: t('SETTINGS_DISABLED'),
|
||||
value: null,
|
||||
},
|
||||
...streamingServer.deviceInfo.content.availableHardwareAccelerations.map((name) => ({
|
||||
...deviceInfo.availableHardwareAccelerations.map((name) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
}))
|
||||
],
|
||||
value: streamingServer.settings.content.transcodeProfile,
|
||||
onSelect: (value) => {
|
||||
value: settings.transcodeProfile,
|
||||
onSelect: (value: string | null) => {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...streamingServer.settings.content,
|
||||
...settings,
|
||||
transcodeProfile: value,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [streamingServer.settings, streamingServer.deviceInfo]);
|
||||
return { streamingServerRemoteUrlInput, remoteEndpointSelect, cacheSizeSelect, torrentProfileSelect, transcodingProfileSelect };
|
||||
}, [settings, deviceInfo]);
|
||||
|
||||
return {
|
||||
streamingServerRemoteUrlInput,
|
||||
remoteEndpointSelect,
|
||||
cacheSizeSelect,
|
||||
torrentProfileSelect,
|
||||
transcodingProfileSelect,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = useStreamingServerSettingsInputs;
|
||||
export default useStreamingOptions;
|
||||
37
src/routes/Settings/components/Category/Category.less
Normal file
37
src/routes/Settings/components/Category/Category.less
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.category {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
overflow: visible;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: thin solid var(--overlay-color);
|
||||
}
|
||||
|
||||
.heading {
|
||||
position: relative;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/routes/Settings/components/Category/Category.tsx
Normal file
26
src/routes/Settings/components/Category/Category.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { t } from 'i18next';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import styles from './Category.less';
|
||||
|
||||
type Props = {
|
||||
icon: string,
|
||||
label: string,
|
||||
children: React.ReactNode,
|
||||
};
|
||||
|
||||
const Category = ({ icon, label, children }: Props) => {
|
||||
return (
|
||||
<div className={styles['category']}>
|
||||
<div className={styles['heading']}>
|
||||
<Icon className={styles['icon']} name={icon} />
|
||||
<div className={styles['label']}>
|
||||
{t(label)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
||||
2
src/routes/Settings/components/Category/index.ts
Normal file
2
src/routes/Settings/components/Category/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Category from './Category';
|
||||
export default Category;
|
||||
16
src/routes/Settings/components/Link/Link.less
Normal file
16
src/routes/Settings/components/Link/Link.less
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
|
||||
.label {
|
||||
color: var(--primary-accent-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/routes/Settings/components/Link/Link.tsx
Normal file
20
src/routes/Settings/components/Link/Link.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'stremio/components';
|
||||
import styles from './Link.less';
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
href?: string,
|
||||
target?: string,
|
||||
onClick?: () => void,
|
||||
};
|
||||
|
||||
const Link = ({ label, href, target, onClick }: Props) => {
|
||||
return (
|
||||
<Button className={styles['link']} title={label} target={target ?? '_blank'} href={href} onClick={onClick}>
|
||||
<div className={styles['label']}>{ label }</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
||||
2
src/routes/Settings/components/Link/index.ts
Normal file
2
src/routes/Settings/components/Link/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Link from './Link';
|
||||
export default Link;
|
||||
78
src/routes/Settings/components/Option/Option.less
Normal file
78
src/routes/Settings/components/Option/Option.less
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
.option {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
overflow: visible;
|
||||
|
||||
.heading, .content {
|
||||
flex: 1 1 50%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
.icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
line-height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
|
||||
:global(.multiselect) {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: var(--overlay-color);
|
||||
}
|
||||
|
||||
:global(.button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3.5rem;
|
||||
width: 100%;
|
||||
padding: 0 2rem;
|
||||
border-radius: 3.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.color-input) {
|
||||
width: 100%;
|
||||
padding: 1.3rem 1rem;
|
||||
border-radius: 3rem;
|
||||
border: 2px solid transparent;
|
||||
transition: 0.3s all ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--overlay-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/routes/Settings/components/Option/Option.tsx
Normal file
36
src/routes/Settings/components/Option/Option.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import styles from './Option.less';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
icon?: string,
|
||||
label: string,
|
||||
children: React.ReactNode,
|
||||
};
|
||||
|
||||
const Option = ({ className, icon, label, children }: Props) => {
|
||||
return (
|
||||
<div className={classNames(className, styles['option'])}>
|
||||
<div className={styles['heading']}>
|
||||
{
|
||||
icon &&
|
||||
<Icon
|
||||
className={styles['icon']}
|
||||
name={icon}
|
||||
/>
|
||||
}
|
||||
<div className={styles['label']}>
|
||||
{t(label)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['content']}>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Option;
|
||||
2
src/routes/Settings/components/Option/index.ts
Normal file
2
src/routes/Settings/components/Option/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Option from './Option';
|
||||
export default Option;
|
||||
22
src/routes/Settings/components/Section/Section.less
Normal file
22
src/routes/Settings/components/Section/Section.less
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.section {
|
||||
position: relative;
|
||||
max-width: 35rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
padding: 3rem 0;
|
||||
overflow: visible;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: thin solid var(--overlay-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
font-size: 1.8rem;
|
||||
line-height: 3.4rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
26
src/routes/Settings/components/Section/Section.tsx
Normal file
26
src/routes/Settings/components/Section/Section.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import styles from './Section.less';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
label?: string,
|
||||
children: React.ReactNode,
|
||||
};
|
||||
|
||||
const Section = forwardRef<HTMLDivElement, Props>(({ className, label, children }: Props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={classNames(className, styles['section'])}>
|
||||
{
|
||||
label &&
|
||||
<div className={styles['label']}>
|
||||
{t(label)}
|
||||
</div>
|
||||
}
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Section;
|
||||
2
src/routes/Settings/components/Section/index.ts
Normal file
2
src/routes/Settings/components/Section/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import Section from './Section';
|
||||
export default Section;
|
||||
11
src/routes/Settings/components/index.ts
Normal file
11
src/routes/Settings/components/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import Category from './Category';
|
||||
import Link from './Link';
|
||||
import Option from './Option';
|
||||
import Section from './Section';
|
||||
|
||||
export {
|
||||
Category,
|
||||
Link,
|
||||
Option,
|
||||
Section,
|
||||
};
|
||||
10
src/routes/Settings/constants.ts
Normal file
10
src/routes/Settings/constants.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const SECTIONS = {
|
||||
GENERAL: 'general',
|
||||
PLAYER: 'player',
|
||||
STREAMING: 'streaming',
|
||||
SHORTCUTS: 'shortcuts',
|
||||
};
|
||||
|
||||
export {
|
||||
SECTIONS,
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Settings = require('./Settings');
|
||||
|
||||
module.exports = Settings;
|
||||
4
src/routes/Settings/index.ts
Normal file
4
src/routes/Settings/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
import Settings from './Settings';
|
||||
export default Settings;
|
||||
|
|
@ -1,466 +0,0 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/components/Toggle/styles.less') {
|
||||
checkbox-icon: icon;
|
||||
}
|
||||
|
||||
:import('~stremio/components/Multiselect/styles.less') {
|
||||
multiselect-menu-container: menu-container;
|
||||
multiselect-label: label;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
height: calc(100% - var(--safe-area-inset-bottom));
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
|
||||
.settings-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.side-menu-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 18rem;
|
||||
padding: 3rem 1.5rem;
|
||||
|
||||
.side-menu-button {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
border-radius: 4rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.4;
|
||||
|
||||
&.selected {
|
||||
font-weight: 600;
|
||||
color: var(--primary-foreground-color);
|
||||
background-color: var(--overlay-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
}
|
||||
|
||||
.spacing {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-info-label {
|
||||
flex: 0 1 auto;
|
||||
margin: 0.5rem 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.3;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sections-container {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
padding: 0 3rem;
|
||||
overflow-y: auto;
|
||||
|
||||
.section-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 3rem 0;
|
||||
overflow: visible;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: thin solid var(--overlay-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
font-size: 1.8rem;
|
||||
line-height: 3.4rem;
|
||||
margin-bottom: 3rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.section-category-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0 1em;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 2.4rem;
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.option-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 35rem;
|
||||
margin-bottom: 2rem;
|
||||
overflow: visible;
|
||||
|
||||
&.link-container {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.user-info-option-container {
|
||||
gap: 1rem;
|
||||
|
||||
.user-info-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
margin-right: 1rem;
|
||||
border: 2px solid var(--primary-accent-color);
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-origin: content-box;
|
||||
background-clip: content-box;
|
||||
opacity: 0.9;
|
||||
background-color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.email-logout-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.email-label-container, .logout-button-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.email-label-container {
|
||||
.email-label {
|
||||
flex: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button-container {
|
||||
&:hover, &:focus {
|
||||
outline: none;
|
||||
|
||||
.logout-label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-label {
|
||||
flex: 1;
|
||||
color: var(--primary-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-panel-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 10rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 3.5rem;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.user-panel-label {
|
||||
flex: 1;
|
||||
max-height: 2.4em;
|
||||
padding: 0 0.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-name-container, .option-input-container {
|
||||
flex: 1 1 50%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
line-height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
&.trakt-icon {
|
||||
.icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: var(--color-trakt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-name-container {
|
||||
justify-content: flex-start;
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.option-input-container {
|
||||
padding: 1rem 1.5rem;
|
||||
|
||||
&.multiselect-container {
|
||||
padding: 0;
|
||||
background: var(--overlay-color);
|
||||
}
|
||||
|
||||
&.button-container {
|
||||
justify-content: center;
|
||||
height: 3.5rem;
|
||||
border-radius: 3.5rem;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&.multiselect-container {
|
||||
>.multiselect-label {
|
||||
line-height: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
}
|
||||
|
||||
.multiselect-menu-container {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.link-input-container {
|
||||
flex: 0 1 auto;
|
||||
padding: 0;
|
||||
|
||||
.label {
|
||||
color: var(--primary-accent-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.checkbox-container {
|
||||
justify-content: center;
|
||||
|
||||
.checkbox-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.color-input-container {
|
||||
padding: 1.3rem 1rem;
|
||||
border-radius: 3rem;
|
||||
border: 2px solid transparent;
|
||||
transition: 0.3s all ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--overlay-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.info-container {
|
||||
justify-content: center;
|
||||
|
||||
&.selectable {
|
||||
user-select: text;
|
||||
|
||||
.label {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.configure-input-container {
|
||||
padding: 0;
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.configure-button-container {
|
||||
flex: none;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--overlay-color);
|
||||
|
||||
&:hover {
|
||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.shortcut-container {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
|
||||
kbd {
|
||||
flex: 0 1 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
border-radius: 0.25em;
|
||||
box-shadow: 0 4px 0 1px var(--modal-background-color);
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 1rem;
|
||||
white-space: nowrap;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.versions-section-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @xsmall) {
|
||||
.settings-container {
|
||||
.settings-content {
|
||||
.side-menu-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sections-container {
|
||||
.versions-section-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.settings-container {
|
||||
.settings-content {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.side-menu-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sections-container {
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.section-container {
|
||||
.user-info-option-container {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.user-panel-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.versions-section-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ const Calendar = require('./Calendar').default;
|
|||
const MetaDetails = require('./MetaDetails');
|
||||
const NotFound = require('./NotFound');
|
||||
const Search = require('./Search');
|
||||
const Settings = require('./Settings');
|
||||
const { default: Settings } = require('./Settings');
|
||||
const Player = require('./Player');
|
||||
const Intro = require('./Intro');
|
||||
|
||||
|
|
|
|||
11
src/services/Shell/Shell.d.ts
vendored
Normal file
11
src/services/Shell/Shell.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
type ShellTransportProps = {
|
||||
shellVersion: string,
|
||||
};
|
||||
|
||||
type ShellTransport = {
|
||||
props: ShellTransportProps,
|
||||
};
|
||||
|
||||
interface ShellService {
|
||||
transport: ShellTransport,
|
||||
}
|
||||
2
src/types/models/Ctx.d.ts
vendored
2
src/types/models/Ctx.d.ts
vendored
|
|
@ -21,6 +21,7 @@ type Settings = {
|
|||
hardwareDecoding: boolean,
|
||||
escExitFullscreen: boolean,
|
||||
interfaceLanguage: string,
|
||||
quitOnClose: boolean,
|
||||
hideSpoilers: boolean,
|
||||
nextVideoNotificationDuration: number,
|
||||
playInBackground: boolean,
|
||||
|
|
@ -41,6 +42,7 @@ type Settings = {
|
|||
subtitlesSize: number,
|
||||
subtitlesTextColor: string,
|
||||
surroundSound: boolean,
|
||||
pauseOnMinimize: boolean,
|
||||
};
|
||||
|
||||
type Profile = {
|
||||
|
|
|
|||
3
src/types/models/DataExport.d.ts
vendored
Normal file
3
src/types/models/DataExport.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
type DataExport = {
|
||||
exportUrl: string | null,
|
||||
};
|
||||
12
src/types/models/StremingServer.d.ts
vendored
12
src/types/models/StremingServer.d.ts
vendored
|
|
@ -23,6 +23,8 @@ type StreamingServerSettings = {
|
|||
cacheRoot: string,
|
||||
cacheSize: number,
|
||||
serverVersion: string,
|
||||
remoteHttps: string | null,
|
||||
transcodeProfile: string | null,
|
||||
};
|
||||
|
||||
type SFile = {
|
||||
|
|
@ -93,6 +95,14 @@ type Statistics = {
|
|||
swarmSize: number,
|
||||
};
|
||||
|
||||
type NetworkInfo = {
|
||||
availableInterfaces: string[],
|
||||
};
|
||||
|
||||
type DeviceInfo = {
|
||||
availableHardwareAccelerations: string[],
|
||||
};
|
||||
|
||||
type PlaybackDevice = {
|
||||
id: string,
|
||||
name: string,
|
||||
|
|
@ -115,4 +125,6 @@ type StreamingServer = {
|
|||
torrent: [string, Loadable<Torrent>] | null,
|
||||
statistics: Loadable<Statistics> | null,
|
||||
playbackDevices: Loadable<PlaybackDevice[]> | null,
|
||||
networkInfo: Loadable<NetworkInfo> | null,
|
||||
deviceInfo: Loadable<DeviceInfo> | null,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue