feat: Support Multiple Server URLs in the settings

This commit is contained in:
Timothy Z. 2024-10-21 17:13:46 +03:00
parent b23204aa34
commit d12766ecad
34 changed files with 925 additions and 191 deletions

View file

@ -0,0 +1,92 @@
// Copyright (C) 2017-2024 Smart code 203358507
.checkbox {
display: flex;
align-items: center;
}
.checkbox label {
display: flex;
align-items: center;
cursor: pointer;
}
.checkbox-container {
position: relative;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--primary-accent-color);
border-radius: 0.25rem;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s, border-color 0.2s;
cursor: pointer;
outline: none;
user-select: none;
&:focus {
outline: var(--focus-outline-size) solid var(--primary-accent-color);
outline-offset: 2px;
}
&:hover {
background-color: var(--overlay-color);
}
}
.checkbox-container input[type='checkbox'] {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.checkbox-icon {
color: var(--primary-foreground-color);
width: 1rem;
}
.checkbox-label {
margin-left: 0.75rem;
color: var(--primary-foreground-color);
font-size: 1rem;
}
.checkbox-checked .checkbox-container {
background-color: var(--primary-accent-color);
border-color: var(--primary-accent-color);
}
.checkbox-checked .checkbox-icon {
color: var(--secondary-foreground-color);
}
.checkbox-unchecked .checkbox-container {
background-color: transparent;
border-color: var(--primary-accent-color);
}
.checkbox-disabled {
cursor: not-allowed;
opacity: 0.6;
}
.checkbox-disabled .checkbox-container {
background-color: var(--overlay-color);
border-color: var(--overlay-color);
}
.checkbox-disabled .checkbox-label {
color: var(--primary-foreground-color);
opacity: 0.6;
}
.checkbox-error .checkbox-container {
border-color: var(--color-reddit);
}
.checkbox-error .checkbox-label {
color: var(--color-reddit);
}

View file

@ -0,0 +1,86 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useState, useEffect, DetailedHTMLProps, HTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from './Checkbox.less';
import Icon from '@stremio/stremio-icons/react';
type Props = {
disabled?: boolean;
value?: boolean;
className?: string;
onChange?: (checked: boolean) => void;
ariaLabel?: string;
error?: string;
};
const Checkbox = ({ disabled, value, className, onChange, ariaLabel, error }: Props) => {
const [isChecked, setIsChecked] = useState(false);
const [isError, setIsError] = useState(false);
const [isDisabled, setIsDisabled] = useState(disabled);
const handleChangeCheckbox = () => {
if (disabled) {
return;
}
setIsChecked(!isChecked);
onChange && onChange(!isChecked);
};
const handleEnterPress = (event: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => {
if ((event.key === 'Enter' || event.key === ' ') && !disabled) {
setIsChecked(!isChecked);
onChange && onChange(!isChecked);
}
};
useEffect(() => setIsDisabled(disabled), [disabled]);
useEffect(() => setIsError(!!error), [error]);
useEffect(() => {
const checked = typeof value === 'boolean' ? value : false;
setIsChecked(checked);
}, [value]);
return (
<>
<div className={styles['checkbox']}>
<label>
<div
className={classNames({
[styles[className || '']]: !!className,
[styles['checkbox-checked']]: isChecked,
[styles['checkbox-unchecked']]: !isChecked,
[styles['checkbox-error']]: isError,
[styles['checkbox-disabled']]: isDisabled,
})}
>
<div
className={styles['checkbox-container']}
role={'input'}
tabIndex={0}
onKeyDown={handleEnterPress}
>
<input
type={'checkbox'}
onChange={handleChangeCheckbox}
aria-label={ariaLabel}
tabIndex={-1}
disabled={disabled}
/>
{
isChecked ?
<Icon name={'checkmark'} className={styles['checkbox-icon']} />
: null
}
</div>
</div>
</label>
</div>
</>
);
};
export default Checkbox;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Checkbox = require('./Checkbox');
module.exports = Checkbox;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Checkbox from './Checkbox';
export default Checkbox;

View file

@ -8,13 +8,13 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput');
const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useSearchHistory = require('./useSearchHistory');
const useLocalSearch = require('./useLocalSearch');
const styles = require('./styles');
const useBinaryState = require('stremio/common/useBinaryState');
const { default: TextInput } = require('stremio/common/TextInput');
const SearchBar = React.memo(({ className, query, active }) => {
const { t } = useTranslation();

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const TextInput = require('stremio/common/TextInput');
const { default: TextInput } = require('../TextInput');
const SearchBarPlaceholder = require('./SearchBarPlaceholder');
const styles = require('./styles');

View file

@ -8,8 +8,8 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const { default: TextInput } = require('../TextInput');
const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput');
const styles = require('./styles');
const SharePrompt = ({ className, url }) => {

View file

@ -1,43 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const styles = require('./styles');
const TextInput = React.forwardRef((props, ref) => {
const onKeyDown = React.useCallback((event) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (event.key === 'Enter' && !event.nativeEvent.submitPrevented && typeof props.onSubmit === 'function') {
props.onSubmit(event);
}
}, [props.onKeyDown, props.onSubmit]);
return (
<input
size={1}
autoCorrect={'off'}
autoCapitalize={'off'}
autoComplete={'off'}
spellCheck={false}
tabIndex={0}
{...props}
ref={ref}
className={classnames(props.className, styles['text-input'], { 'disabled': props.disabled })}
onKeyDown={onKeyDown}
/>
);
});
TextInput.displayName = 'TextInput';
TextInput.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool,
onKeyDown: PropTypes.func,
onSubmit: PropTypes.func
};
module.exports = TextInput;

View file

@ -0,0 +1,49 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React from 'react';
import classnames from 'classnames';
import styles from './styles.less';
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
className?: string;
disabled?: boolean;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onSubmit?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
};
const TextInput = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { onSubmit, className, disabled, ...rest } = props;
const onKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (
event.key === 'Enter' &&
!(event.nativeEvent as any).submitPrevented &&
typeof onSubmit === 'function'
) {
onSubmit(event);
}
}, [props.onKeyDown, onSubmit]);
return (
<input
size={1}
autoCorrect={'off'}
autoCapitalize={'off'}
autoComplete={'off'}
spellCheck={false}
tabIndex={0}
ref={ref}
className={classnames(className, styles['text-input'], { disabled })}
onKeyDown={onKeyDown}
{...rest}
/>
);
});
TextInput.displayName = 'TextInput';
export default TextInput;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const TextInput = require('./TextInput');
module.exports = TextInput;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import TextInput from './TextInput';
export default TextInput;

View file

@ -6,21 +6,21 @@ const classnames = require('classnames');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const Checkbox = React.forwardRef(({ className, checked, children, ...props }, ref) => {
const Toggle = React.forwardRef(({ className, checked, children, ...props }, ref) => {
return (
<Button {...props} ref={ref} className={classnames(className, styles['checkbox-container'], { 'checked': checked })}>
<Button {...props} ref={ref} className={classnames(className, styles['toggle-container'], { 'checked': checked })}>
<div className={styles['toggle']} />
{children}
</Button>
);
});
Checkbox.displayName = 'Checkbox';
Toggle.displayName = 'Toggle';
Checkbox.propTypes = {
Toggle.propTypes = {
className: PropTypes.string,
checked: PropTypes.bool,
children: PropTypes.node
};
module.exports = Checkbox;
module.exports = Toggle;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Toggle = require('./Toggle');
module.exports = Toggle;

View file

@ -8,7 +8,7 @@
@thumb-size: calc(@height - @thumb-margin);
.checkbox-container {
.toggle-container {
position: relative;
.toggle {

View file

@ -2,7 +2,7 @@
const AddonDetailsModal = require('./AddonDetailsModal');
const Button = require('./Button');
const Checkbox = require('./Checkbox');
const Toggle = require('./Toggle');
const { default: Chips } = require('./Chips');
const ColorInput = require('./ColorInput');
const ContinueWatchingItem = require('./ContinueWatchingItem');
@ -25,7 +25,7 @@ const SearchBar = require('./SearchBar');
const StreamingServerWarning = require('./StreamingServerWarning');
const SharePrompt = require('./SharePrompt');
const Slider = require('./Slider');
const TextInput = require('./TextInput');
const { default: TextInput } = require('./TextInput');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const comparatorWithPriorities = require('./comparatorWithPriorities');
@ -47,11 +47,12 @@ const useStreamingServer = require('./useStreamingServer');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const EventModal = require('./EventModal');
const { default: Checkbox } = require('./Checkbox');
module.exports = {
AddonDetailsModal,
Button,
Checkbox,
Toggle,
Chips,
ColorInput,
ContinueWatchingItem,
@ -77,6 +78,7 @@ module.exports = {
SharePrompt,
Slider,
TextInput,
Checkbox,
ToastProvider,
useToast,
TooltipProvider,

View file

@ -0,0 +1,96 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useCallback } from 'react';
import { useServices } from 'stremio/services';
import { useToast } from './Toast';
import useModelState from './useModelState';
import useProfile from './useProfile';
const useStreamingServerUrls = () => {
const { core } = useServices();
const profile = useProfile();
const toast = useToast();
const ctx = useModelState({ model: 'ctx' });
const streamingServerUrls = ctx.streamingServerUrls.sort((a, b) => {
const dateA = new Date(a._mtime).getTime();
const dateB = new Date(b._mtime).getTime();
return dateA - dateB;
})
const onAdd = useCallback((url) => {
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
};
if (isValidUrl(url)) {
toast.show({
type: 'success',
title: 'New URL added',
message: 'The new URL has been added successfully',
timeout: 4000
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddServerUrl',
args: url,
}
});
} else {
toast.show({
type: 'error',
title: 'Invalid URL',
message: 'Please provide a valid URL',
timeout: 4000
});
}
}, []);
const onDelete = useCallback((url) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DeleteServerUrl',
args: url,
}
});
}, []);
const onSelect = useCallback((url) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
streamingServerUrl: url
}
}
});
}, []);
const onReload = useCallback(() => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'Reload'
}
});
}, []);
const actions = {
onAdd,
onDelete,
onSelect,
onReload
}
return { streamingServerUrls, actions };
};
export default useStreamingServerUrls;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const ConsentCheckbox = require('./ConsentCheckbox');
module.exports = ConsentCheckbox;

View file

@ -3,11 +3,11 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Button, Checkbox } = require('stremio/common');
const { Button, Toggle } = require('stremio/common');
const styles = require('./styles');
const ConsentCheckbox = React.forwardRef(({ className, label, link, href, onToggle, ...props }, ref) => {
const checkboxOnClick = React.useCallback((event) => {
const ConsentToggle = React.forwardRef(({ className, label, link, href, onToggle, ...props }, ref) => {
const toggleOnClick = React.useCallback((event) => {
if (typeof props.onClick === 'function') {
props.onClick(event);
}
@ -24,7 +24,7 @@ const ConsentCheckbox = React.forwardRef(({ className, label, link, href, onTogg
event.nativeEvent.togglePrevented = true;
}, []);
return (
<Checkbox {...props} ref={ref} className={classnames(className, styles['consent-checkbox-container'])} onClick={checkboxOnClick}>
<Toggle {...props} ref={ref} className={classnames(className, styles['consent-toogle-container'])} onClick={toggleOnClick}>
<div className={styles['label']}>
{label}
{' '}
@ -37,13 +37,13 @@ const ConsentCheckbox = React.forwardRef(({ className, label, link, href, onTogg
null
}
</div>
</Checkbox>
</Toggle>
);
});
ConsentCheckbox.displayName = 'ConsentCheckbox';
ConsentToggle.displayName = 'ConsentToggle';
ConsentCheckbox.propTypes = {
ConsentToggle.propTypes = {
className: PropTypes.string,
checked: PropTypes.bool,
label: PropTypes.string,
@ -53,4 +53,4 @@ ConsentCheckbox.propTypes = {
onClick: PropTypes.func
};
module.exports = ConsentCheckbox;
module.exports = ConsentToggle;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2023 Smart code 203358507
const ConsentToggle = require('./ConsentToggle');
module.exports = ConsentToggle;

View file

@ -2,11 +2,11 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
:import('~stremio/common/Checkbox/styles.less') {
:import('~stremio/common/Toggle/styles.less') {
checkbox-icon: icon;
}
.consent-checkbox-container {
.consent-toogle-container {
display: flex;
flex-direction: row;
align-items: center;

View file

@ -2,7 +2,7 @@
const React = require('react');
const PropTypes = require('prop-types');
const { TextInput } = require('stremio/common');
const { default: TextInput } = require('stremio/common/TextInput');
const CredentialsTextInput = React.forwardRef((props, ref) => {
const onKeyDown = React.useCallback((event) => {

View file

@ -9,7 +9,7 @@ const { Modal, useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { Button, Image, useBinaryState } = require('stremio/common');
const CredentialsTextInput = require('./CredentialsTextInput');
const ConsentCheckbox = require('./ConsentCheckbox');
const ConsentToggle = require('./ConsentToggle');
const PasswordResetModal = require('./PasswordResetModal');
const useFacebookLogin = require('./useFacebookLogin');
const styles = require('./styles');
@ -54,7 +54,7 @@ const Intro = ({ queryParams }) => {
error: '',
[action.name]: action.value
};
case 'toggle-checkbox':
case 'toogle-checkbox':
return {
...state,
error: '',
@ -210,13 +210,13 @@ const Intro = ({ queryParams }) => {
termsRef.current.focus();
}, []);
const toggleTermsAccepted = React.useCallback(() => {
dispatch({ type: 'toggle-checkbox', name: 'termsAccepted' });
dispatch({ type: 'toogle-checkbox', name: 'termsAccepted' });
}, []);
const togglePrivacyPolicyAccepted = React.useCallback(() => {
dispatch({ type: 'toggle-checkbox', name: 'privacyPolicyAccepted' });
dispatch({ type: 'toogle-checkbox', name: 'privacyPolicyAccepted' });
}, []);
const toggleMarketingAccepted = React.useCallback(() => {
dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' });
dispatch({ type: 'toogle-checkbox', name: 'marketingAccepted' });
}, []);
const switchFormOnClick = React.useCallback(() => {
const queryParams = new URLSearchParams([['form', state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM]]);
@ -307,27 +307,27 @@ const Intro = ({ queryParams }) => {
onChange={confirmPasswordOnChange}
onSubmit={confirmPasswordOnSubmit}
/>
<ConsentCheckbox
<ConsentToggle
ref={termsRef}
className={styles['consent-checkbox']}
className={styles['consent-toggle']}
label={'I have read and agree with the Stremio'}
link={'Terms and conditions'}
href={'https://www.stremio.com/tos'}
checked={state.termsAccepted}
onToggle={toggleTermsAccepted}
/>
<ConsentCheckbox
<ConsentToggle
ref={privacyPolicyRef}
className={styles['consent-checkbox']}
className={styles['consent-toggle']}
label={'I have read and agree with the Stremio'}
link={'Privacy Policy'}
href={'https://www.stremio.com/privacy'}
checked={state.privacyPolicyAccepted}
onToggle={togglePrivacyPolicyAccepted}
/>
<ConsentCheckbox
<ConsentToggle
ref={marketingRef}
className={styles['consent-checkbox']}
className={styles['consent-toggle']}
label={'I agree to receive marketing communications from Stremio'}
checked={state.marketingAccepted}
onToggle={toggleMarketingAccepted}

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { t } = require('i18next');
const { Image, SearchBar, Checkbox } = require('stremio/common');
const { Image, SearchBar, Toggle } = require('stremio/common');
const SeasonsBar = require('./SeasonsBar');
const Video = require('./Video');
const styles = require('./styles');
@ -84,9 +84,9 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
<React.Fragment>
{
showNotificationsToggle && libraryItem ?
<Checkbox className={styles['notifications-checkbox']} checked={!libraryItem.state.noNotif} onClick={toggleNotifications}>
<Toggle className={styles['notifications-toggle']} checked={!libraryItem.state.noNotif} onClick={toggleNotifications}>
{t('DETAIL_RECEIVE_NOTIF_SERIES')}
</Checkbox>
</Toggle>
:
null
}

View file

@ -36,7 +36,7 @@
}
}
.notifications-checkbox {
.notifications-toggle {
flex: none;
display: flex;
flex-direction: row;

View file

@ -7,11 +7,12 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, usePlatform, useStreamingServer, useBinaryState, withCoreSuspender, useToast, useModelState } = require('stremio/common');
const { Button, Toggle, MainNavBars, Multiselect, ColorInput, useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common');
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';
@ -35,16 +36,15 @@ const Settings = () => {
subtitlesBackgroundColorInput,
subtitlesOutlineColorInput,
audioLanguageSelect,
surroundSoundCheckbox,
surroundSoundToggle,
seekTimeDurationSelect,
seekShortTimeDurationSelect,
escExitFullscreenCheckbox,
escExitFullscreenToggle,
playInExternalPlayerSelect,
nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
hardwareDecodingCheckbox,
streamingServerUrlInput
bingeWatchingToggle,
playInBackgroundToggle,
hardwareDecodingToggle,
} = useProfileSettingsInputs(profile);
const {
streamingServerRemoteUrlInput,
@ -53,34 +53,11 @@ const Settings = () => {
torrentProfileSelect,
transcodingProfileSelect,
} = useStreamingServerSettingsInputs(streamingServer);
const [configureServerUrlModalOpen, openConfigureServerUrlModal, closeConfigureServerUrlModal] = useBinaryState(false);
const configureServerUrlInputRef = React.useRef(null);
const configureServerUrlOnSubmit = React.useCallback(() => {
streamingServerUrlInput.onChange(configureServerUrlInputRef.current.value);
closeConfigureServerUrlModal();
}, [streamingServerUrlInput]);
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 configureServerUrlModalButtons = React.useMemo(() => {
return [
{
className: styles['cancel-button'],
label: 'Cancel',
props: {
onClick: closeConfigureServerUrlModal
}
},
{
label: 'Submit',
props: {
onClick: configureServerUrlOnSubmit,
}
}
];
}, [configureServerUrlOnSubmit]);
const logoutButtonOnClick = React.useCallback(() => {
core.transport.dispatch({
action: 'Ctx',
@ -118,14 +95,6 @@ const Settings = () => {
const exportDataOnClick = React.useCallback(() => {
loadDataExport();
}, []);
const reloadStreamingServer = React.useCallback(() => {
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'Reload'
}
});
}, []);
const onCopyRemoteUrlClick = React.useCallback(() => {
if (streamingServer.remoteUrl) {
navigator.clipboard.writeText(streamingServer.remoteUrl);
@ -192,11 +161,7 @@ const Settings = () => {
if (routeFocused) {
updateSelectedSectionId();
}
closeConfigureServerUrlModal();
}, [routeFocused]);
const ctx = useModelState({ model: 'ctx' });
console.log(profile); // eslint-disable-line no-console
console.log(ctx); // eslint-disable-line no-console
return (
<MainNavBars className={styles['settings-container']} route={'settings'}>
<div className={classnames(styles['settings-content'], 'animation-fade-in')}>
@ -372,9 +337,9 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_FULLSCREEN_EXIT') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
{...escExitFullscreenCheckbox}
<Toggle
className={classnames(styles['option-input-container'], styles['toogle-container'])}
{...escExitFullscreenToggle}
/>
</div>
:
@ -435,10 +400,10 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SURROUND_SOUND') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
<Toggle
className={classnames(styles['option-input-container'], styles['toogle-container'])}
tabIndex={-1}
{...surroundSoundCheckbox}
{...surroundSoundToggle}
/>
</div>
</div>
@ -469,11 +434,11 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_BACKGROUND') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
<Toggle
className={classnames(styles['option-input-container'], styles['toogle-container'])}
disabled={true}
tabIndex={-1}
{...playInBackgroundCheckbox}
{...playInBackgroundToggle}
/>
</div>
</div>
@ -486,9 +451,9 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('AUTO_PLAY') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
{...bingeWatchingCheckbox}
<Toggle
className={classnames(styles['option-input-container'], styles['toogle-container'])}
{...bingeWatchingToggle}
/>
</div>
<div className={styles['option-container']}>
@ -520,22 +485,18 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
<Toggle
className={classnames(styles['option-input-container'], styles['toogle-container'])}
disabled={true}
tabIndex={-1}
{...hardwareDecodingCheckbox}
{...hardwareDecodingToggle}
/>
</div>
</div>
<div ref={streamingServerSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Reload'} onClick={reloadStreamingServer}>
<div className={styles['label']}>{ t('RELOAD') }</div>
</Button>
</div>
<div className={styles['option-container']}>
<URLsManager />
{/* <div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('STATUS') }</div>
</div>
@ -555,8 +516,8 @@ const Settings = () => {
}
</div>
</div>
</div>
<div className={styles['option-container']}>
</div> */}
{/* <div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Url</div>
</div>
@ -566,7 +527,7 @@ const Settings = () => {
<Icon className={styles['icon']} name={'settings'} />
</Button>
</div>
</div>
</div> */}
{
streamingServerRemoteUrlInput.value !== null ?
<div className={styles['option-container']}>
@ -782,7 +743,7 @@ const Settings = () => {
</div>
</div>
</div>
{
{/* {
configureServerUrlModalOpen ?
<ModalDialog
className={styles['configure-server-url-modal-container']}
@ -801,7 +762,7 @@ const Settings = () => {
</ModalDialog>
:
null
}
} */}
</MainNavBars>
);
};

View file

@ -0,0 +1,194 @@
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
.item {
display: flex;
padding: 1rem 1.5rem;
border-radius: var(--border-radius);
transition: 0.3s all ease-in-out;
background-color: transparent;
border: 2px solid transparent;
justify-content: space-between;
position: relative;
.content {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
.checkbox {
}
.label {
color: var(--primary-foreground-color);
}
}
.actions {
display: flex;
gap: 1rem;
margin-right: 5rem;
.status {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
.icon {
width: 0.75rem;
height: 0.75rem;
border-radius: 1rem;
&.ready {
background-color: var(--secondary-accent-color);
}
&.error {
background-color: var(--color-trakt);
}
}
.label {
font-size: 1rem;
color: var(--primary-foreground-color);
}
}
.delete {
position: absolute;
right: 1.5rem;
top: 50%;
display: none;
gap: 0.5rem;
padding: 0.25rem;
align-items: center;
justify-content: center;
background-color: transparent;
transition: 0.3s all ease-in-out;
border-radius: var(--border-radius);
transform: translateY(-50%);
width: 3rem;
opacity: 0.6;
.icon {
width: 2rem;
height: 2rem;
color: var(--primary-foreground-color);
}
&:hover {
background-color: var(--overlay-color);
opacity: 1;
.icon {
color: var(--color-trakt);
}
}
}
}
&.add {
padding: 0.5rem 1.5rem;
gap: 1rem;
.input {
background-color: var(--overlay-color);
border-radius: var(--border-radius);
color: var(--primary-foreground-color);
padding: 0.5rem 0.75rem;
border: 1px solid transparent;
width: 70%;
&:focus {
border: 1px solid var(--primary-foreground-color);
}
}
.actions {
display: flex;
gap: 0.25rem;
margin-right: 0;
.add, .cancel {
display: flex;
gap: 0.5rem;
padding: 0.25rem;
align-items: center;
justify-content: center;
background-color: transparent;
transition: 0.3s all ease-in-out;
border-radius: var(--border-radius);
width: 3rem;
opacity: 0.6;
.icon {
width: 2rem;
height: 2rem;
color: var(--primary-foreground-color);
}
&:hover {
opacity: 1;
background-color: var(--overlay-color);
}
}
.add {
.icon {
width: 1.8rem;
height: 1.8rem;
}
&:hover {
.icon {
color: var(--secondary-accent-color);
}
}
}
.cancel {
&:hover {
.icon {
color: var(--color-trakt);
}
}
}
}
&:hover {
border: 2px solid transparent;
background-color: var(--overlay-color);
}
}
&:hover {
background-color: var(--overlay-color);
.actions {
.delete {
display: flex;
}
}
}
}
@media only screen and (max-width: @minimum) {
.item {
padding: 1rem 0.5rem;
.actions {
margin-right: 4rem;
.delete {
right: 0.5rem;
}
}
&.add {
padding: 0.5rem;
}
}
}

View file

@ -0,0 +1,148 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useState, useCallback, ChangeEvent } from 'react';
import { useProfile } from 'stremio/common';
import Button from 'stremio/common/Button';
import useStreamingServer from 'stremio/common/useStreamingServer';
import TextInput from 'stremio/common/TextInput';
import Icon from '@stremio/stremio-icons/react';
import styles from './Item.less';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import Checkbox from 'stremio/common/Checkbox';
type ViewModeProps = {
url: string;
onDelete?: (url: string) => void;
onSelect?: (url: string) => void;
}
const ViewMode = ({ url, onDelete, onSelect }: ViewModeProps) => {
const { t } = useTranslation();
const streamingServer = useStreamingServer();
const profile = useProfile();
const selected = profile.settings.streamingServerUrl === url;
const handleDelete = () => {
onDelete?.(url);
};
const handleSelect = () => {
onSelect?.(url);
};
return (
<>
<div className={styles['content']}>
<Checkbox value={selected} onChange={handleSelect} />
<div className={styles['label']}>{url}</div>
</div>
<div className={styles['actions']}>
{
selected ?
<div className={styles['status']}>
<div className={classNames(styles['icon'], { [styles['ready']]: streamingServer.settings?.type === 'Ready' }, { [styles['error']]: streamingServer.settings?.type === 'Err' })} />
<div className={styles['label']}>
{
streamingServer.settings === null ?
'NotLoaded'
:
streamingServer.settings.type === 'Ready' ?
t('SETTINGS_SERVER_STATUS_ONLINE')
:
streamingServer.settings.type === 'Err' ?
t('SETTINGS_SERVER_STATUS_ERROR')
:
streamingServer.settings.type
}
</div>
</div>
: null
}
<Button className={styles['delete']} onClick={handleDelete}>
<Icon name={'close'} className={styles['icon']} />
</Button>
</div>
</>
);
};
type AddModeProps = {
inputValue: string;
handleValueChange: (event: ChangeEvent<HTMLInputElement>) => void;
onAdd?: (url: string) => void;
onCancel?: () => void;
}
const AddMode = ({ inputValue, handleValueChange, onAdd, onCancel }: AddModeProps) => {
const handleAdd = () => {
if (inputValue.trim()) {
onAdd?.(inputValue);
}
};
return (
<>
<TextInput
className={styles['input']}
value={inputValue}
onChange={handleValueChange}
/>
<div className={styles['actions']}>
<Button className={styles['add']} onClick={handleAdd}>
<Icon name={'checkmark'} className={styles['icon']} />
</Button>
<Button className={styles['cancel']} onClick={onCancel}>
<Icon name={'close'} className={styles['icon']} />
</Button>
</div>
</>
);
};
type Props =
| {
mode: 'add';
onAdd?: (url: string) => void;
onCancel?: () => void;
}
| {
mode: 'view';
url: string;
onDelete?: (url: string) => void;
onSelect?: (url: string) => void;
};
const Item = (props: Props) => {
if (props.mode === 'add') {
const { onAdd, onCancel } = props;
const [inputValue, setInputValue] = useState('');
const handleValueChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
}, []);
return (
<div className={classNames(styles['item'], styles['add'])}>
<AddMode
inputValue={inputValue}
handleValueChange={handleValueChange}
onAdd={onAdd}
onCancel={onCancel}
/>
</div>
);
} else if (props.mode === 'view') {
const { url, onDelete, onSelect } = props;
return (
<div className={classNames(styles['item'])}>
<ViewMode url={url} onDelete={onDelete} onSelect={onSelect} />
</div>
);
}
};
export default Item;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Item from './Item'
export default Item;

View file

@ -0,0 +1,81 @@
// Copyright (C) 2017-2024 Smart code 203358507
.wrapper {
display: flex;
flex-direction: column;
max-width: 35rem;
margin-bottom: 2rem;
.header {
display: flex;
justify-content: space-around;
align-items: center;
.label {
font-size: 1rem;
color: var(--primary-foreground-color);
font-weight: 400;
opacity: 0.6;
}
}
.content {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.5rem 0;
}
.footer {
display: flex;
justify-content: space-between;
.add-url {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
padding: 0.5rem 1.5rem;
background-color: var(--secondary-accent-color);
transition: 0.3s all ease-in-out;
border-radius: 1.5rem;
color: var(--primary-foreground-color);
border: 2px solid transparent;
.icon {
width: 1rem;
height: 1rem;
color: var(--primary-foreground-color);
}
&:hover {
background-color: transparent;
border: 2px solid var(--primary-foreground-color);
}
}
.reload {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
padding: 0.5rem 1.5rem;
background-color: var(--overlay-color);
border-radius: 1.5rem;
transition: 0.3s all ease-in-out;
color: var(--primary-foreground-color);
border: 2px solid transparent;
.icon {
width: 1rem;
height: 1rem;
color: var(--primary-foreground-color);
}
&:hover {
background-color: transparent;
border: 2px solid var(--primary-foreground-color);
}
}
}
}

View file

@ -0,0 +1,61 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next';
import styles from './URLsManager.less';
import Button from 'stremio/common/Button';
import Item from './Item';
import Icon from '@stremio/stremio-icons/react';
import useStreamingServerUrls from 'stremio/common/useStreamingServerUrls';
const URLsManager = () => {
const { t } = useTranslation();
const [addMode, setAddMode] = useState(false);
const { streamingServerUrls, actions } = useStreamingServerUrls();
const onAdd = () => {
setAddMode(true);
}
const onCancel = () => {
setAddMode(false);
};
const handleAddUrl = useCallback((url: string) => {
actions.onAdd(url);
setAddMode(false);
}, []);
return (
<div className={styles['wrapper']}>
<div className={styles['header']}>
<div className={styles['label']}>URL</div>
<div className={styles['label']}>{ t('STATUS') }</div>
</div>
<div className={styles['content']}>
{
streamingServerUrls.map((url: StreamingServerUrl, index: number) => (
<Item mode={'view'} key={index} {...url} {...actions} />
))
}
{
addMode ?
<Item mode={'add'} onAdd={handleAddUrl} onCancel={onCancel} />
: null
}
</div>
<div className={styles['footer']}>
<Button label={'Add URL'} className={styles['add-url']} onClick={onAdd}>
<Icon name={'add'} className={styles['icon']} />
{ t('ADD_URL') }
</Button>
<Button className={styles['reload']} title={'Reload'} onClick={actions.onReload}>
<Icon name={'reset'} className={styles['icon']} />
<div className={styles['label']}>{t('RELOAD')}</div>
</Button>
</div>
</div>
);
};
export default URLsManager;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2024 Smart code 203358507
import URLsManager from './URLsManager';
export default URLsManager;

View file

@ -3,7 +3,7 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/Checkbox/styles.less') {
:import('~stremio/common/Toggle/styles.less') {
checkbox-icon: icon;
}

View file

@ -136,7 +136,7 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const surroundSoundCheckbox = React.useMemo(() => ({
const surroundSoundToggle = React.useMemo(() => ({
checked: profile.settings.surroundSound,
onClick: () => {
core.transport.dispatch({
@ -151,7 +151,7 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const escExitFullscreenCheckbox = React.useMemo(() => ({
const escExitFullscreenToggle = React.useMemo(() => ({
checked: profile.settings.escExitFullscreen,
onClick: () => {
core.transport.dispatch({
@ -261,7 +261,7 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const bingeWatchingCheckbox = React.useMemo(() => ({
const bingeWatchingToggle = React.useMemo(() => ({
checked: profile.settings.bingeWatching,
onClick: () => {
core.transport.dispatch({
@ -276,7 +276,7 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const playInBackgroundCheckbox = React.useMemo(() => ({
const playInBackgroundToggle = React.useMemo(() => ({
checked: profile.settings.playInBackground,
onClick: () => {
core.transport.dispatch({
@ -291,7 +291,7 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const hardwareDecodingCheckbox = React.useMemo(() => ({
const hardwareDecodingToggle = React.useMemo(() => ({
checked: profile.settings.hardwareDecoding,
onClick: () => {
core.transport.dispatch({
@ -306,21 +306,6 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const streamingServerUrlInput = React.useMemo(() => ({
value: profile.settings.streamingServerUrl,
onChange: (value) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
streamingServerUrl: value
}
}
});
}
}), [profile.settings]);
return {
interfaceLanguageSelect,
subtitlesLanguageSelect,
@ -329,16 +314,15 @@ const useProfileSettingsInputs = (profile) => {
subtitlesBackgroundColorInput,
subtitlesOutlineColorInput,
audioLanguageSelect,
surroundSoundCheckbox,
escExitFullscreenCheckbox,
surroundSoundToggle,
escExitFullscreenToggle,
seekTimeDurationSelect,
seekShortTimeDurationSelect,
playInExternalPlayerSelect,
nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
hardwareDecodingCheckbox,
streamingServerUrlInput
bingeWatchingToggle,
playInBackgroundToggle,
hardwareDecodingToggle,
};
};

View file

@ -67,8 +67,16 @@ type SearchHistoryItem = {
type SearchHistory = SearchHistoryItem[];
type StreamingServerUrl = {
url: string,
_mtime: Date,
};
type StreamingServerUrls = StreamingServerUrl[];
type Ctx = {
profile: Profile,
notifications: Notifications,
searchHistory: SearchHistory,
streamingServerUrls: StreamingServerUrls,
};