From d12766ecad4f668f89357d7b0614007a38b90e2a Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 21 Oct 2024 17:13:46 +0300 Subject: [PATCH] feat: Support Multiple Server URLs in the settings --- src/common/Checkbox/Checkbox.less | 92 +++++++++ src/common/Checkbox/Checkbox.tsx | 86 ++++++++ src/common/Checkbox/index.js | 5 - src/common/Checkbox/index.ts | 5 + .../HorizontalNavBar/SearchBar/SearchBar.js | 2 +- src/common/SearchBar/SearchBar.js | 2 +- src/common/SharePrompt/SharePrompt.js | 2 +- src/common/TextInput/TextInput.js | 43 ---- src/common/TextInput/TextInput.tsx | 49 +++++ src/common/TextInput/index.js | 5 - src/common/TextInput/index.ts | 5 + .../Checkbox.js => Toggle/Toggle.js} | 10 +- src/common/Toggle/index.js | 5 + src/common/{Checkbox => Toggle}/styles.less | 2 +- src/common/index.js | 8 +- src/common/useStreamingServerUrls.js | 96 +++++++++ src/routes/Intro/ConsentCheckbox/index.js | 5 - .../ConsentToggle.js} | 16 +- src/routes/Intro/ConsentToggle/index.js | 5 + .../styles.less | 4 +- .../CredentialsTextInput.js | 2 +- src/routes/Intro/Intro.js | 22 +- .../MetaDetails/VideosList/VideosList.js | 6 +- src/routes/MetaDetails/VideosList/styles.less | 2 +- src/routes/Settings/Settings.js | 97 +++------ .../Settings/URLsManager/Item/Item.less | 194 ++++++++++++++++++ src/routes/Settings/URLsManager/Item/Item.tsx | 148 +++++++++++++ src/routes/Settings/URLsManager/Item/index.ts | 5 + .../Settings/URLsManager/URLsManager.less | 81 ++++++++ .../Settings/URLsManager/URLsManager.tsx | 61 ++++++ src/routes/Settings/URLsManager/index.ts | 5 + src/routes/Settings/styles.less | 2 +- .../Settings/useProfileSettingsInputs.js | 36 +--- src/types/models/Ctx.d.ts | 8 + 34 files changed, 925 insertions(+), 191 deletions(-) create mode 100644 src/common/Checkbox/Checkbox.less create mode 100644 src/common/Checkbox/Checkbox.tsx delete mode 100644 src/common/Checkbox/index.js create mode 100644 src/common/Checkbox/index.ts delete mode 100644 src/common/TextInput/TextInput.js create mode 100644 src/common/TextInput/TextInput.tsx delete mode 100644 src/common/TextInput/index.js create mode 100644 src/common/TextInput/index.ts rename src/common/{Checkbox/Checkbox.js => Toggle/Toggle.js} (68%) create mode 100644 src/common/Toggle/index.js rename src/common/{Checkbox => Toggle}/styles.less (98%) create mode 100644 src/common/useStreamingServerUrls.js delete mode 100644 src/routes/Intro/ConsentCheckbox/index.js rename src/routes/Intro/{ConsentCheckbox/ConsentCheckbox.js => ConsentToggle/ConsentToggle.js} (74%) create mode 100644 src/routes/Intro/ConsentToggle/index.js rename src/routes/Intro/{ConsentCheckbox => ConsentToggle}/styles.less (90%) create mode 100644 src/routes/Settings/URLsManager/Item/Item.less create mode 100644 src/routes/Settings/URLsManager/Item/Item.tsx create mode 100644 src/routes/Settings/URLsManager/Item/index.ts create mode 100644 src/routes/Settings/URLsManager/URLsManager.less create mode 100644 src/routes/Settings/URLsManager/URLsManager.tsx create mode 100644 src/routes/Settings/URLsManager/index.ts diff --git a/src/common/Checkbox/Checkbox.less b/src/common/Checkbox/Checkbox.less new file mode 100644 index 000000000..8dea5fd1d --- /dev/null +++ b/src/common/Checkbox/Checkbox.less @@ -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); +} diff --git a/src/common/Checkbox/Checkbox.tsx b/src/common/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..2148e0dd9 --- /dev/null +++ b/src/common/Checkbox/Checkbox.tsx @@ -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, 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 ( + <> +
+ +
+ + ); +}; + +export default Checkbox; diff --git a/src/common/Checkbox/index.js b/src/common/Checkbox/index.js deleted file mode 100644 index b185f6cbc..000000000 --- a/src/common/Checkbox/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const Checkbox = require('./Checkbox'); - -module.exports = Checkbox; diff --git a/src/common/Checkbox/index.ts b/src/common/Checkbox/index.ts new file mode 100644 index 000000000..40b9097d9 --- /dev/null +++ b/src/common/Checkbox/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Checkbox from './Checkbox'; + +export default Checkbox; diff --git a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index 5db06f1d0..40cfbed06 100644 --- a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -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(); diff --git a/src/common/SearchBar/SearchBar.js b/src/common/SearchBar/SearchBar.js index 3b7013aa0..23ad327e0 100644 --- a/src/common/SearchBar/SearchBar.js +++ b/src/common/SearchBar/SearchBar.js @@ -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'); diff --git a/src/common/SharePrompt/SharePrompt.js b/src/common/SharePrompt/SharePrompt.js index 2d25682e4..4d49087c8 100644 --- a/src/common/SharePrompt/SharePrompt.js +++ b/src/common/SharePrompt/SharePrompt.js @@ -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 }) => { diff --git a/src/common/TextInput/TextInput.js b/src/common/TextInput/TextInput.js deleted file mode 100644 index 742e87dfc..000000000 --- a/src/common/TextInput/TextInput.js +++ /dev/null @@ -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 ( - - ); -}); - -TextInput.displayName = 'TextInput'; - -TextInput.propTypes = { - className: PropTypes.string, - disabled: PropTypes.bool, - onKeyDown: PropTypes.func, - onSubmit: PropTypes.func -}; - -module.exports = TextInput; diff --git a/src/common/TextInput/TextInput.tsx b/src/common/TextInput/TextInput.tsx new file mode 100644 index 000000000..b409f229c --- /dev/null +++ b/src/common/TextInput/TextInput.tsx @@ -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 & { + className?: string; + disabled?: boolean; + onKeyDown?: (event: React.KeyboardEvent) => void; + onSubmit?: (event: React.KeyboardEvent) => void; +}; + +const TextInput = React.forwardRef((props, ref) => { + const { onSubmit, className, disabled, ...rest } = props; + + const onKeyDown = React.useCallback((event: React.KeyboardEvent) => { + 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 ( + + ); +}); + +TextInput.displayName = 'TextInput'; + +export default TextInput; diff --git a/src/common/TextInput/index.js b/src/common/TextInput/index.js deleted file mode 100644 index a61d9ca79..000000000 --- a/src/common/TextInput/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const TextInput = require('./TextInput'); - -module.exports = TextInput; diff --git a/src/common/TextInput/index.ts b/src/common/TextInput/index.ts new file mode 100644 index 000000000..60cbf8e67 --- /dev/null +++ b/src/common/TextInput/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import TextInput from './TextInput'; + +export default TextInput; diff --git a/src/common/Checkbox/Checkbox.js b/src/common/Toggle/Toggle.js similarity index 68% rename from src/common/Checkbox/Checkbox.js rename to src/common/Toggle/Toggle.js index a823ce488..837944747 100644 --- a/src/common/Checkbox/Checkbox.js +++ b/src/common/Toggle/Toggle.js @@ -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 ( - ); }); -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; diff --git a/src/common/Toggle/index.js b/src/common/Toggle/index.js new file mode 100644 index 000000000..ae6c69d8a --- /dev/null +++ b/src/common/Toggle/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const Toggle = require('./Toggle'); + +module.exports = Toggle; diff --git a/src/common/Checkbox/styles.less b/src/common/Toggle/styles.less similarity index 98% rename from src/common/Checkbox/styles.less rename to src/common/Toggle/styles.less index d8224db53..e1c2e9d0a 100644 --- a/src/common/Checkbox/styles.less +++ b/src/common/Toggle/styles.less @@ -8,7 +8,7 @@ @thumb-size: calc(@height - @thumb-margin); -.checkbox-container { +.toggle-container { position: relative; .toggle { diff --git a/src/common/index.js b/src/common/index.js index 5adef3e60..6387821f0 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, diff --git a/src/common/useStreamingServerUrls.js b/src/common/useStreamingServerUrls.js new file mode 100644 index 000000000..36c8f8b9e --- /dev/null +++ b/src/common/useStreamingServerUrls.js @@ -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; diff --git a/src/routes/Intro/ConsentCheckbox/index.js b/src/routes/Intro/ConsentCheckbox/index.js deleted file mode 100644 index 85376bae3..000000000 --- a/src/routes/Intro/ConsentCheckbox/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const ConsentCheckbox = require('./ConsentCheckbox'); - -module.exports = ConsentCheckbox; diff --git a/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js b/src/routes/Intro/ConsentToggle/ConsentToggle.js similarity index 74% rename from src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js rename to src/routes/Intro/ConsentToggle/ConsentToggle.js index cdb5dd189..9940a817f 100644 --- a/src/routes/Intro/ConsentCheckbox/ConsentCheckbox.js +++ b/src/routes/Intro/ConsentToggle/ConsentToggle.js @@ -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 ( - +
{label} {' '} @@ -37,13 +37,13 @@ const ConsentCheckbox = React.forwardRef(({ className, label, link, href, onTogg null }
-
+ ); }); -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; diff --git a/src/routes/Intro/ConsentToggle/index.js b/src/routes/Intro/ConsentToggle/index.js new file mode 100644 index 000000000..8edfe4a27 --- /dev/null +++ b/src/routes/Intro/ConsentToggle/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const ConsentToggle = require('./ConsentToggle'); + +module.exports = ConsentToggle; diff --git a/src/routes/Intro/ConsentCheckbox/styles.less b/src/routes/Intro/ConsentToggle/styles.less similarity index 90% rename from src/routes/Intro/ConsentCheckbox/styles.less rename to src/routes/Intro/ConsentToggle/styles.less index 2bf136924..494c9cc35 100644 --- a/src/routes/Intro/ConsentCheckbox/styles.less +++ b/src/routes/Intro/ConsentToggle/styles.less @@ -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; diff --git a/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js b/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js index 61c558cb0..36d26e880 100644 --- a/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js +++ b/src/routes/Intro/CredentialsTextInput/CredentialsTextInput.js @@ -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) => { diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index bcdd74aff..35397c0ab 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -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} /> - - - { showNotificationsToggle && libraryItem ? - + {t('DETAIL_RECEIVE_NOTIF_SERIES')} - + : null } diff --git a/src/routes/MetaDetails/VideosList/styles.less b/src/routes/MetaDetails/VideosList/styles.less index e1b2215ae..9f9a7edee 100644 --- a/src/routes/MetaDetails/VideosList/styles.less +++ b/src/routes/MetaDetails/VideosList/styles.less @@ -36,7 +36,7 @@ } } - .notifications-checkbox { + .notifications-toggle { flex: none; display: flex; flex-direction: row; diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 0fe3094ef..5d1b024ff 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -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 (
@@ -372,9 +337,9 @@ const Settings = () => {
{ t('SETTINGS_FULLSCREEN_EXIT') }
-
: @@ -435,10 +400,10 @@ const Settings = () => {
{ t('SETTINGS_SURROUND_SOUND') }
- @@ -469,11 +434,11 @@ const Settings = () => {
{ t('SETTINGS_PLAY_IN_BACKGROUND') }
- @@ -486,9 +451,9 @@ const Settings = () => {
{ t('AUTO_PLAY') }
-
@@ -520,22 +485,18 @@ const Settings = () => {
{ t('SETTINGS_HWDEC') }
-
{ t('SETTINGS_NAV_STREAMING') }
-
- -
-
+ + {/*
{ t('STATUS') }
@@ -555,8 +516,8 @@ const Settings = () => { }
-
-
+
*/} + {/*
Url
@@ -566,7 +527,7 @@ const Settings = () => {
- + */} { streamingServerRemoteUrlInput.value !== null ?
@@ -782,7 +743,7 @@ const Settings = () => {
- { + {/* { configureServerUrlModalOpen ? { : null - } + } */}
); }; diff --git a/src/routes/Settings/URLsManager/Item/Item.less b/src/routes/Settings/URLsManager/Item/Item.less new file mode 100644 index 000000000..8ef93763e --- /dev/null +++ b/src/routes/Settings/URLsManager/Item/Item.less @@ -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; + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/URLsManager/Item/Item.tsx b/src/routes/Settings/URLsManager/Item/Item.tsx new file mode 100644 index 000000000..9858a3a81 --- /dev/null +++ b/src/routes/Settings/URLsManager/Item/Item.tsx @@ -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 ( + <> +
+ +
{url}
+
+
+ { + selected ? +
+
+
+ { + 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 + } +
+
+ : null + } + +
+ + ); +}; + +type AddModeProps = { + inputValue: string; + handleValueChange: (event: ChangeEvent) => void; + onAdd?: (url: string) => void; + onCancel?: () => void; +} + +const AddMode = ({ inputValue, handleValueChange, onAdd, onCancel }: AddModeProps) => { + const handleAdd = () => { + if (inputValue.trim()) { + onAdd?.(inputValue); + } + }; + + return ( + <> + +
+ + +
+ + ); +}; + +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) => { + setInputValue(event.target.value); + }, []); + + return ( +
+ +
+ ); + } else if (props.mode === 'view') { + const { url, onDelete, onSelect } = props; + + return ( +
+ +
+ ); + } +}; + +export default Item; + diff --git a/src/routes/Settings/URLsManager/Item/index.ts b/src/routes/Settings/URLsManager/Item/index.ts new file mode 100644 index 000000000..87c19a210 --- /dev/null +++ b/src/routes/Settings/URLsManager/Item/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Item from './Item' + +export default Item; diff --git a/src/routes/Settings/URLsManager/URLsManager.less b/src/routes/Settings/URLsManager/URLsManager.less new file mode 100644 index 000000000..a68660681 --- /dev/null +++ b/src/routes/Settings/URLsManager/URLsManager.less @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/routes/Settings/URLsManager/URLsManager.tsx b/src/routes/Settings/URLsManager/URLsManager.tsx new file mode 100644 index 000000000..82578e346 --- /dev/null +++ b/src/routes/Settings/URLsManager/URLsManager.tsx @@ -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 ( +
+
+
URL
+
{ t('STATUS') }
+
+
+ { + streamingServerUrls.map((url: StreamingServerUrl, index: number) => ( + + )) + } + { + addMode ? + + : null + } +
+
+ + +
+
+ ); +}; + +export default URLsManager; diff --git a/src/routes/Settings/URLsManager/index.ts b/src/routes/Settings/URLsManager/index.ts new file mode 100644 index 000000000..c8874c0a4 --- /dev/null +++ b/src/routes/Settings/URLsManager/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import URLsManager from './URLsManager'; + +export default URLsManager; diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less index 7c9444509..5480db8b5 100644 --- a/src/routes/Settings/styles.less +++ b/src/routes/Settings/styles.less @@ -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; } diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index a90d0d483..d36b169f9 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -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, }; }; diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts index 33da9f366..9619a0e89 100644 --- a/src/types/models/Ctx.d.ts +++ b/src/types/models/Ctx.d.ts @@ -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, };