From 45818c12738a3b18f764b000224fa54e97de7d49 Mon Sep 17 00:00:00 2001 From: Dreamacro <8615343+Dreamacro@users.noreply.github.com> Date: Fri, 20 Jan 2023 14:13:25 +0800 Subject: [PATCH] Feature: add basic multi hosts support --- src/components/Button/index.tsx | 8 ++-- src/components/Button/style.scss | 1 + src/components/Message/index.tsx | 5 ++- src/components/Modal/index.tsx | 13 ++++-- src/components/Modal/style.scss | 12 ------ src/components/Select/index.tsx | 21 +++++----- src/components/Select/style.scss | 1 + src/containers/Connections/index.tsx | 2 +- .../ExternalControllerDrawer/index.tsx | 41 ++++++++++++++++--- src/containers/Settings/index.tsx | 32 +++++++++++---- src/i18n/en_US.ts | 3 ++ src/i18n/zh_CN.ts | 3 ++ src/stores/jotai.ts | 6 +-- src/stores/request.ts | 12 +++--- 14 files changed, 106 insertions(+), 54 deletions(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 58c7dd2..61fa8a9 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -8,19 +8,19 @@ import './style.scss' interface ButtonProps extends BaseComponentProps { type?: 'primary' | 'normal' | 'danger' | 'success' | 'warning' onClick?: MouseEventHandler - disiabled?: boolean + disabled?: boolean } export function Button (props: ButtonProps) { - const { type = 'normal', onClick = noop, children, className, style, disiabled } = props - const classname = classnames('button', `button-${type}`, className, { 'button-disabled': disiabled }) + const { type = 'normal', onClick = noop, children, className, style, disabled } = props + const classname = classnames('button', `button-${type}`, className, { 'button-disabled': disabled }) return ( ) } diff --git a/src/components/Button/style.scss b/src/components/Button/style.scss index eb3ba27..668bdb9 100644 --- a/src/components/Button/style.scss +++ b/src/components/Button/style.scss @@ -7,6 +7,7 @@ font-size: 14px; cursor: pointer; transition: all 150ms ease; + user-select: none; &:focus { outline: none; diff --git a/src/components/Message/index.tsx b/src/components/Message/index.tsx index 3f18598..e4537e3 100644 --- a/src/components/Message/index.tsx +++ b/src/components/Message/index.tsx @@ -1,6 +1,7 @@ import classnames from 'classnames' import { useLayoutEffect } from 'react' -import { unmountComponentAtNode, render } from 'react-dom' +import { unmountComponentAtNode } from 'react-dom' +import { createRoot } from 'react-dom/client' import { Icon } from '@components' import { noop } from '@lib/helper' @@ -89,7 +90,7 @@ export function showMessage (args: ArgsProps) { onClose, } - render(, container) + createRoot(container).render() } export const info = ( diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index a19439d..6a54538 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -27,6 +27,9 @@ interface ModalProps extends BaseComponentProps { // show footer footer?: boolean + // footer extra + footerExtra?: React.ReactNode + // on click ok onOk?: typeof noop @@ -45,6 +48,7 @@ export function Modal (props: ModalProps) { bodyClassName, bodyStyle, className, + footerExtra, style, children, } = props @@ -84,9 +88,12 @@ export function Modal (props: ModalProps) { >{children} { footer && ( -
- - +
+ {footerExtra} +
+ + +
) } diff --git a/src/components/Modal/style.scss b/src/components/Modal/style.scss index a23551b..d58f279 100644 --- a/src/components/Modal/style.scss +++ b/src/components/Modal/style.scss @@ -47,18 +47,6 @@ $mobileBigWidth: 480px; font-size: 14px; color: $color-primary-darken; } - - .footer { - width: 100%; - margin: 5px 0; - display: flex; - align-items: center; - justify-content: flex-end; - - .button { - margin-left: 10px; - } - } } .modal-small { diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index 8e6fab6..c550e22 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames' -import { useRef, useState, useMemo, useLayoutEffect } from 'react' +import { useRef, useState, useMemo, useLayoutEffect, ReactNode } from 'react' import { createPortal } from 'react-dom' import { Icon } from '@components' @@ -9,7 +9,7 @@ import { BaseComponentProps } from '@models' import './style.scss' export interface SelectOptions { - label: string + label: ReactNode value: T disabled?: boolean key?: React.Key @@ -37,13 +37,6 @@ export function Select (props: SelectProps) { const [showDropDownList, setShowDropDownList] = useState(false) const [dropdownListStyles, setDropdownListStyles] = useState({}) - useLayoutEffect(() => { - const targetRectInfo = targetRef.current!.getBoundingClientRect() - setDropdownListStyles({ - top: Math.floor(targetRectInfo.top + targetRectInfo.height) + 6, - left: Math.floor(targetRectInfo.left) - 10, - }) - }, []) useLayoutEffect(() => { const current = portalRef.current @@ -57,6 +50,14 @@ export function Select (props: SelectProps) { if (disabled) { return } + + if (!showDropDownList) { + const targetRectInfo = targetRef.current!.getBoundingClientRect() + setDropdownListStyles({ + top: Math.floor(targetRectInfo.top + targetRectInfo.height) + 6, + left: Math.floor(targetRectInfo.left) - 10, + }) + } setShowDropDownList(!showDropDownList) } @@ -98,7 +99,7 @@ export function Select (props: SelectProps) { ref={targetRef} onClick={handleShowDropList} > - {matchChild?.label} + {matchChild?.label}
{createPortal(dropDownList, portalRef.current)} diff --git a/src/components/Select/style.scss b/src/components/Select/style.scss index 47acb1c..75c49ba 100644 --- a/src/components/Select/style.scss +++ b/src/components/Select/style.scss @@ -30,6 +30,7 @@ background: $color-white; padding: 0; transition: all 200ms ease; + border-radius: 4px; > .option { color: $color-primary-darken; diff --git a/src/containers/Connections/index.tsx b/src/containers/Connections/index.tsx index b166634..c5634e0 100644 --- a/src/containers/Connections/index.tsx +++ b/src/containers/Connections/index.tsx @@ -302,7 +302,7 @@ export default function Connections () {
- +
diff --git a/src/containers/ExternalControllerDrawer/index.tsx b/src/containers/ExternalControllerDrawer/index.tsx index 3aae051..6607399 100644 --- a/src/containers/ExternalControllerDrawer/index.tsx +++ b/src/containers/ExternalControllerDrawer/index.tsx @@ -1,11 +1,10 @@ -import { useAtom } from 'jotai' -import { useUpdateAtom } from 'jotai/utils' +import { useAtom, useSetAtom } from 'jotai' import { useEffect } from 'react' -import { Modal, Input, Alert } from '@components' +import { Modal, Input, Alert, Button, error } from '@components' import { useObject } from '@lib/hook' import { useI18n, useAPIInfo, identityAtom } from '@stores' -import { localStorageAtom } from '@stores/request' +import { hostSelectIdxStorageAtom, hostsStorageAtom } from '@stores/request' import './style.scss' export default function ExternalController () { @@ -23,18 +22,50 @@ export default function ExternalController () { set({ hostname, port, secret }) }, [hostname, port, secret, set]) - const setter = useUpdateAtom(localStorageAtom) + const [hosts, setter] = useAtom(hostsStorageAtom) + const [hostSelectIdx, setHostSelectIdx] = useAtom(hostSelectIdxStorageAtom) function handleOk () { const { hostname, port, secret } = value setter([{ hostname, port, secret }]) } + function handleAdd () { + const { hostname, port, secret } = value + const nextHosts = [...hosts, { hostname, port, secret }] + setter(nextHosts) + setHostSelectIdx(nextHosts.length - 1) + } + + function handleDelete () { + const { hostname, port } = value + const idx = hosts.findIndex(h => h.hostname === hostname && h.port === port) + if (idx === -1) { + error(t('externalControllerSetting.deleteErrorText')) + return + } + + const nextHosts = [...hosts.slice(0, idx), ...hosts.slice(idx + 1)] + setter(nextHosts) + if (hostSelectIdx >= idx) { + setHostSelectIdx(0) + } + } + + const footerExtra = ( +
+ + +
+ ) + return ( setIdentity(true)} onOk={handleOk} > diff --git a/src/containers/Settings/index.tsx b/src/containers/Settings/index.tsx index 9c53c44..cc603d9 100644 --- a/src/containers/Settings/index.tsx +++ b/src/containers/Settings/index.tsx @@ -1,13 +1,13 @@ import classnames from 'classnames' -import { useUpdateAtom } from 'jotai/utils' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { capitalize } from 'lodash-es' import { useEffect, useMemo } from 'react' -import { Header, Card, Switch, ButtonSelect, ButtonSelectOptions, Input } from '@components' +import { Header, Card, Switch, ButtonSelect, ButtonSelectOptions, Input, Select } from '@components' import { Lang } from '@i18n' import { useObject } from '@lib/hook' import { jsBridge } from '@lib/jsBridge' -import { useI18n, useClashXData, useAPIInfo, useGeneral, useVersion, useClient, identityAtom } from '@stores' +import { useI18n, useClashXData, useAPIInfo, useGeneral, useVersion, useClient, identityAtom, hostSelectIdxStorageAtom, hostsStorageAtom } from '@stores' import './style.scss' const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh_CN' }, { label: 'English', value: 'en_US' }] @@ -16,7 +16,9 @@ export default function Settings () { const { premium } = useVersion() const { data: clashXData, update: fetchClashXData } = useClashXData() const { general, update: fetchGeneral } = useGeneral() - const setIdentity = useUpdateAtom(identityAtom) + const setIdentity = useSetAtom(identityAtom) + const [hostSelectIdx, setHostSelectIdx] = useAtom(hostSelectIdxStorageAtom) + const hostsStorage = useAtomValue(hostsStorageAtom) const apiInfo = useAPIInfo() const { translation, setLang, lang } = useI18n() const { t } = translation('Settings') @@ -95,6 +97,10 @@ export default function Settings () { return options }, [t, premium]) + const controllerOptions = hostsStorage.map( + (h, idx) => ({ value: idx, label: {h.hostname} }), + ) + return (
@@ -171,11 +177,19 @@ export default function Settings () {
{t('labels.externalController')} - !isClashX && setIdentity(false)}> - {`${externalControllerHost}:${externalControllerPort}`} - +
+