diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index 7e6a2149f..2286f6bf2 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -1,4 +1,5 @@ import cx from 'clsx'; +import { useAtom } from 'jotai'; import * as React from 'react'; import { Pause, Play } from 'react-feather'; import { useTranslation } from 'react-i18next'; @@ -9,7 +10,7 @@ import LogSearch from 'src/components/LogSearch'; import { connect, useStoreActions } from 'src/components/StateProvider'; import SvgYacd from 'src/components/SvgYacd'; import useRemainingViewPortHeight from 'src/hooks/useRemainingViewPortHeight'; -import { getLogStreamingPaused, useApiConfig } from 'src/store/app'; +import { logStreamingPausedAtom, useApiConfig } from 'src/store/app'; import { getLogLevel } from 'src/store/configs'; import { appendLog, getLogsForDisplay } from 'src/store/logs'; import { DispatchFn, Log, State } from 'src/store/types'; @@ -64,21 +65,19 @@ function Logs({ dispatch, logLevel, logs, - logStreamingPaused, }: { dispatch: DispatchFn; logLevel: string; logs: Log[]; - logStreamingPaused: boolean; }) { + const [logStreamingPaused, setLogStreamingPaused] = useAtom(logStreamingPausedAtom); const apiConfig = useApiConfig(); - const actions = useStoreActions(); const toggleIsRefreshPaused = useCallback(() => { logStreamingPaused ? reconnectLogs({ ...apiConfig, logLevel }) : stopLogs(); // being lazy here // ideally we should check the result of previous operation before updating this - actions.app.updateAppConfig('logStreamingPaused', !logStreamingPaused); - }, [apiConfig, logLevel, logStreamingPaused, actions.app]); + setLogStreamingPaused(!logStreamingPaused); + }, [apiConfig, logLevel, logStreamingPaused, setLogStreamingPaused]); const appendLogInternal = useCallback((log: Log) => dispatch(appendLog(log)), [dispatch]); useEffect(() => { fetchLogs({ ...apiConfig, logLevel }, appendLogInternal); @@ -130,7 +129,6 @@ function Logs({ const mapState = (s: State) => ({ logs: getLogsForDisplay(s), logLevel: getLogLevel(s), - logStreamingPaused: getLogStreamingPaused(s), }); export default connect(mapState)(Logs); diff --git a/src/components/proxies/ProxyGroup.tsx b/src/components/proxies/ProxyGroup.tsx index 0a0a808d7..292965a46 100644 --- a/src/components/proxies/ProxyGroup.tsx +++ b/src/components/proxies/ProxyGroup.tsx @@ -7,7 +7,12 @@ import CollapsibleSectionHeader from '$src/components/CollapsibleSectionHeader'; import { ZapAnimated } from '$src/components/shared/ZapAnimated'; import { connect, useStoreActions } from '$src/components/StateProvider'; import { useState2 } from '$src/hooks/basic'; -import { collapsibleIsOpenAtom, getHideUnavailableProxies, getProxySortBy } from '$src/store/app'; +import { + autoCloseOldConnsAtom, + collapsibleIsOpenAtom, + hideUnavailableProxiesAtom, + proxySortByAtom, +} from '$src/store/app'; import { getProxies, switchProxy } from '$src/store/proxies'; import { DelayMapping, DispatchFn, ProxiesMapping, State } from '$src/store/types'; import { ClashAPIConfig } from '$src/types'; @@ -22,8 +27,6 @@ type ProxyGroupImplProps = { name: string; all: string[]; delay: DelayMapping; - hideUnavailableProxies: boolean; - proxySortBy: string; proxies: ProxiesMapping; type: string; now: string; @@ -35,8 +38,6 @@ function ProxyGroupImpl({ name, all: allItems, delay, - hideUnavailableProxies, - proxySortBy, proxies, type, now, @@ -45,6 +46,8 @@ function ProxyGroupImpl({ }: ProxyGroupImplProps) { const [collapsibleIsOpen, setCollapsibleIsOpen] = useAtom(collapsibleIsOpenAtom); const isOpen = collapsibleIsOpen[`proxyGroup:${name}`]; + const [proxySortBy] = useAtom(proxySortByAtom); + const [hideUnavailableProxies] = useAtom(hideUnavailableProxiesAtom); const all = useFilteredAndSorted(allItems, delay, hideUnavailableProxies, proxySortBy, proxies); const isSelectable = useMemo(() => type === 'Selector', [type]); const { @@ -59,13 +62,13 @@ function ProxyGroupImpl({ const toggle = useCallback(() => { updateCollapsibleIsOpen('proxyGroup', name, !isOpen); }, [isOpen, updateCollapsibleIsOpen, name]); - + const [autoCloseOldConns] = useAtom(autoCloseOldConnsAtom); const itemOnTapCallback = useCallback( (proxyName: string) => { if (!isSelectable) return; - dispatch(switchProxy(apiConfig, name, proxyName)); + dispatch(switchProxy(apiConfig, name, proxyName, autoCloseOldConns)); }, - [apiConfig, dispatch, name, isSelectable], + [apiConfig, dispatch, name, isSelectable, autoCloseOldConns], ); const testingLatency = useState2(false); @@ -108,15 +111,11 @@ function ProxyGroupImpl({ export const ProxyGroup = connect((s: State, { name, delay }) => { const proxies = getProxies(s); - const proxySortBy = getProxySortBy(s); - const hideUnavailableProxies = getHideUnavailableProxies(s); const group = proxies[name]; const { all, type, now } = group; return { all, delay, - hideUnavailableProxies, - proxySortBy, proxies, type, now, diff --git a/src/components/proxies/ProxyProvider.tsx b/src/components/proxies/ProxyProvider.tsx index 7424c49e7..07bd80b7a 100644 --- a/src/components/proxies/ProxyProvider.tsx +++ b/src/components/proxies/ProxyProvider.tsx @@ -10,8 +10,8 @@ import { connect } from 'src/components/StateProvider'; import { framerMotionResource } from 'src/misc/motion'; import { collapsibleIsOpenAtom, - getHideUnavailableProxies, - getProxySortBy, + hideUnavailableProxiesAtom, + proxySortByAtom, useApiConfig, } from 'src/store/app'; import { getDelay, healthcheckProviderByName } from 'src/store/proxies'; @@ -30,32 +30,21 @@ type Props = { name: string; proxies: string[]; delay: DelayMapping; - hideUnavailableProxies: boolean; - proxySortBy: string; type: 'Proxy' | 'Rule'; vehicleType: 'HTTP' | 'File' | 'Compatible'; updatedAt?: string; dispatch: (x: any) => Promise; }; -function ProxyProviderImpl({ - name, - proxies: all, - delay, - hideUnavailableProxies, - proxySortBy, - vehicleType, - updatedAt, - dispatch, -}: Props) { +function ProxyProviderImpl({ name, proxies: all, delay, vehicleType, updatedAt, dispatch }: Props) { const [collapsibleIsOpen, setCollapsibleIsOpen] = useAtom(collapsibleIsOpenAtom); const isOpen = collapsibleIsOpen[`proxyProvider:${name}`]; + const [proxySortBy] = useAtom(proxySortByAtom); + const [hideUnavailableProxies] = useAtom(hideUnavailableProxiesAtom); const apiConfig = useApiConfig(); const proxies = useFilteredAndSorted(all, delay, hideUnavailableProxies, proxySortBy); const checkingHealth = useState2(false); - const updateProvider = useUpdateProviderItem({ dispatch, apiConfig, name }); - const healthcheckProvider = useCallback(() => { if (checkingHealth.value) return; checkingHealth.set(true); @@ -132,14 +121,10 @@ function Refresh() { } const mapState = (s: State, { proxies }) => { - const hideUnavailableProxies = getHideUnavailableProxies(s); const delay = getDelay(s); - const proxySortBy = getProxySortBy(s); return { proxies, delay, - hideUnavailableProxies, - proxySortBy, }; }; diff --git a/src/components/proxies/Settings.tsx b/src/components/proxies/Settings.tsx index e66b25d12..5c1604888 100644 --- a/src/components/proxies/Settings.tsx +++ b/src/components/proxies/Settings.tsx @@ -1,12 +1,11 @@ +import { useAtom } from 'jotai'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import Select from 'src/components/shared/Select'; import { ToggleInput } from '$src/components/form/Toggle'; -import { State, StateApp } from '$src/store/types'; +import Select from '$src/components/shared/Select'; +import { autoCloseOldConnsAtom, hideUnavailableProxiesAtom, proxySortByAtom } from '$src/store/app'; -import { getAutoCloseOldConns, getHideUnavailableProxies, getProxySortBy } from '../../store/app'; -import { connect, useStoreActions } from '../StateProvider'; import s from './Settings.module.scss'; const options = [ @@ -19,23 +18,20 @@ const options = [ const { useCallback } = React; -function Settings({ appConfig }: { appConfig: StateApp }) { - const { - app: { updateAppConfig }, - } = useStoreActions(); - +export default function Settings() { + const [autoCloseOldConns, setAutoCloseOldConns] = useAtom(autoCloseOldConnsAtom); + const [proxySortBy, setProxySortBy] = useAtom(proxySortByAtom); + const [hideUnavailableProxies, setHideUnavailableProxies] = useAtom(hideUnavailableProxiesAtom); const handleProxySortByOnChange = useCallback( - (e: React.ChangeEvent) => { - updateAppConfig('proxySortBy', e.target.value); - }, - [updateAppConfig], + (e: React.ChangeEvent) => setProxySortBy(e.target.value), + [setProxySortBy], ); const handleHideUnavailablesSwitchOnChange = useCallback( (v: boolean) => { - updateAppConfig('hideUnavailableProxies', v); + setHideUnavailableProxies(v); }, - [updateAppConfig], + [setHideUnavailableProxies], ); const { t } = useTranslation(); return ( @@ -47,7 +43,7 @@ function Settings({ appConfig }: { appConfig: StateApp }) { options={options.map((o) => { return [o[0], t(o[1])]; })} - selected={appConfig.proxySortBy} + selected={proxySortBy} onChange={handleProxySortByOnChange} /> @@ -58,7 +54,7 @@ function Settings({ appConfig }: { appConfig: StateApp }) {
@@ -68,26 +64,11 @@ function Settings({ appConfig }: { appConfig: StateApp }) {
updateAppConfig('autoCloseOldConns', v)} + checked={autoCloseOldConns} + onChange={setAutoCloseOldConns} />
); } - -const mapState = (s: State) => { - const proxySortBy = getProxySortBy(s); - const hideUnavailableProxies = getHideUnavailableProxies(s); - const autoCloseOldConns = getAutoCloseOldConns(s); - - return { - appConfig: { - proxySortBy, - hideUnavailableProxies, - autoCloseOldConns, - }, - }; -}; -export default connect(mapState)(Settings); diff --git a/src/components/shared/ThemeSwitcher.tsx b/src/components/shared/ThemeSwitcher.tsx index 882d98019..738b06a5b 100644 --- a/src/components/shared/ThemeSwitcher.tsx +++ b/src/components/shared/ThemeSwitcher.tsx @@ -1,17 +1,20 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button'; import { Tooltip } from '@reach/tooltip'; import cx from 'clsx'; +import { useAtom } from 'jotai'; import * as React from 'react'; import { Check } from 'react-feather'; import { useTranslation } from 'react-i18next'; -import { connect } from 'src/components/StateProvider'; -import { framerMotionResource } from 'src/misc/motion'; -import { getTheme, switchTheme } from 'src/store/app'; -import { DispatchFn, State } from 'src/store/types'; + +import { framerMotionResource } from '$src/misc/motion'; +import { saveStateTmp } from '$src/misc/storage'; +import { setTheme, themeAtom } from '$src/store/app'; +import { ThemeType } from '$src/store/types'; import s from './ThemeSwitcher.module.scss'; -function ThemeSwitcherImpl({ theme, dispatch }: { theme: string; dispatch: DispatchFn }) { +export function ThemeSwitcher() { + const [theme, setThemeAtom] = useAtom(themeAtom); const { t } = useTranslation(); const themeIcon = React.useMemo(() => { @@ -28,7 +31,11 @@ function ThemeSwitcherImpl({ theme, dispatch }: { theme: string; dispatch: Dispa } }, [theme]); - const onSelect = React.useCallback((v: string) => dispatch(switchTheme(v)), [dispatch]); + const onSelect = React.useCallback((v: ThemeType) => { + setThemeAtom(v); + setTheme(v); + saveStateTmp({ theme: v }); + }, [setThemeAtom]); return ( @@ -149,6 +156,3 @@ function Auto() { ); } - -const mapState = (s: State) => ({ theme: getTheme(s) }); -export const ThemeSwitcher = connect(mapState)(ThemeSwitcherImpl); diff --git a/src/store/app.ts b/src/store/app.ts index b0c798e45..7cf53fc13 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -1,20 +1,11 @@ import { atom, useAtom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; -import { loadState, saveState } from '$src/misc/storage'; +import { loadState } from '$src/misc/storage'; import { trimTrailingSlash } from '$src/misc/utils'; -import { - ClashAPIConfigWithAddedAt, - DispatchFn, - GetStateFn, - State, - StateApp, -} from '$src/store/types'; +import { ClashAPIConfigWithAddedAt, StateApp, ThemeType } from '$src/store/types'; import { ClashAPIConfig } from '$src/types'; -import { fetchConfigs } from './configs'; -import { closeModal } from './modals'; - let iState: StateApp; const STORAGE_KEY = { @@ -22,7 +13,6 @@ const STORAGE_KEY = { }; const rootEl = document.querySelector('html'); -type ThemeType = 'dark' | 'light' | 'auto'; const defaultClashAPIConfig = { baseURL: document.getElementById('app')?.getAttribute('data-base-url') ?? 'http://127.0.0.1:9090', @@ -30,7 +20,6 @@ const defaultClashAPIConfig = { addedAt: 0, }; -// type Theme = 'light' | 'dark'; const defaultState: StateApp = { selectedClashAPIConfigIndex: 0, clashAPIConfigs: [defaultClashAPIConfig], @@ -52,11 +41,16 @@ const CONFIG_QUERY_PARAMS = ['hostname', 'port', 'secret', 'theme']; // atoms -export const selectedClashAPIConfigIndexAtom = atom(initialState().selectedClashAPIConfigIndex); -export const clashAPIConfigsAtom = atom(initialState().clashAPIConfigs); +export const selectedClashAPIConfigIndexAtom = atom(initialState().selectedClashAPIConfigIndex); +export const clashAPIConfigsAtom = atom(initialState().clashAPIConfigs); export const selectedChartStyleIndexAtom = atom(initialState().selectedChartStyleIndex); export const latencyTestUrlAtom = atom(initialState().latencyTestUrl); export const collapsibleIsOpenAtom = atom(initialState().collapsibleIsOpen); +export const proxySortByAtom = atom(initialState().proxySortBy); +export const hideUnavailableProxiesAtom = atom(initialState().hideUnavailableProxies); +export const autoCloseOldConnsAtom = atom(initialState().autoCloseOldConns); +export const themeAtom = atom(initialState().theme); +export const logStreamingPausedAtom = atom(initialState().logStreamingPaused); // hooks @@ -66,12 +60,6 @@ export function useApiConfig() { return apiConfigs[idx]; } -export const getTheme = (s: State) => s.app.theme; -export const getProxySortBy = (s: State) => s.app.proxySortBy; -export const getHideUnavailableProxies = (s: State) => s.app.hideUnavailableProxies; -export const getAutoCloseOldConns = (s: State) => s.app.autoCloseOldConns; -export const getLogStreamingPaused = (s: State) => s.app.logStreamingPaused; - export function findClashAPIConfigIndexTmp( arr: ClashAPIConfigWithAddedAt[], { baseURL, secret, metaLabel }: ClashAPIConfig, @@ -83,18 +71,18 @@ export function findClashAPIConfigIndexTmp( } // unused -export function updateClashAPIConfig(conf: ClashAPIConfig) { - return async (dispatch: DispatchFn, getState: GetStateFn) => { - const clashAPIConfig = conf; - dispatch('appUpdateClashAPIConfig', (s) => { - s.app.clashAPIConfigs[0] = clashAPIConfig; - }); - // side effect - saveState(getState().app); - dispatch(closeModal('apiConfig')); - dispatch(fetchConfigs(clashAPIConfig)); - }; -} +// export function updateClashAPIConfig(conf: ClashAPIConfig) { +// return async (dispatch: DispatchFn, getState: GetStateFn) => { +// const clashAPIConfig = conf; +// dispatch('appUpdateClashAPIConfig', (s) => { +// s.app.clashAPIConfigs[0] = clashAPIConfig; +// }); +// // side effect +// saveState(getState().app); +// dispatch(closeModal('apiConfig')); +// dispatch(fetchConfigs(clashAPIConfig)); +// }; +// } function insertThemeColorMeta(color: string, media?: string) { const meta0 = document.createElement('meta'); @@ -135,7 +123,7 @@ function updateMetaThemeColor(theme: ThemeType) { } } -function setTheme(theme: ThemeType = 'dark') { +export function setTheme(theme: ThemeType = 'dark') { if (theme === 'auto') { rootEl.setAttribute('data-theme', 'auto'); } else if (theme === 'dark') { @@ -146,30 +134,6 @@ function setTheme(theme: ThemeType = 'dark') { updateMetaThemeColor(theme); } -export function switchTheme(nextTheme = 'auto') { - return (dispatch: DispatchFn, getState: GetStateFn) => { - const currentTheme = getTheme(getState()); - if (currentTheme === nextTheme) return; - // side effect - setTheme(nextTheme as ThemeType); - dispatch('storeSwitchTheme', (s) => { - s.app.theme = nextTheme; - }); - // side effect - saveState(getState().app); - }; -} - -export function updateAppConfig(name: string, value: unknown) { - return (dispatch: DispatchFn, getState: GetStateFn) => { - dispatch('appUpdateAppConfig', (s) => { - s.app[name] = value; - }); - // side effect - saveState(getState().app); - }; -} - function parseConfigQueryString() { const { search } = window.location; const collector: Record = {}; diff --git a/src/store/index.ts b/src/store/index.ts index 743858a0f..34876057c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,6 +1,5 @@ import { initialState as app, - updateAppConfig, } from './app'; import { initialState as configs } from './configs'; import { initialState as logs } from './logs'; @@ -16,9 +15,5 @@ export const initialState = { }; export const actions = { - updateAppConfig, - app: { - updateAppConfig, - }, proxies: proxiesActions, }; diff --git a/src/store/proxies.tsx b/src/store/proxies.tsx index aa4b19373..0b99babdd 100644 --- a/src/store/proxies.tsx +++ b/src/store/proxies.tsx @@ -16,7 +16,6 @@ import { ClashAPIConfig } from 'src/types'; import * as connAPI from '../api/connections'; import * as proxiesAPI from '../api/proxies'; -import { getAutoCloseOldConns } from './app'; export const initialState: StateProxies = { proxies: {}, @@ -188,6 +187,7 @@ async function switchProxyImpl( apiConfig: ClashAPIConfig, groupName: string, itemName: string, + autoCloseOldConns: boolean, ) { try { const res = await proxiesAPI.requestToSwitchProxy(apiConfig, groupName, itemName); @@ -201,7 +201,6 @@ async function switchProxyImpl( } dispatch(fetchProxies(apiConfig)); - const autoCloseOldConns = getAutoCloseOldConns(getState()); if (autoCloseOldConns) { // use fresh state const proxies = getProxies(getState()); @@ -251,10 +250,17 @@ function closePrevConnsAndTheModal(apiConfig: ClashAPIConfig) { }; } -export function switchProxy(apiConfig: ClashAPIConfig, groupName: string, itemName: string) { +export function switchProxy( + apiConfig: ClashAPIConfig, + groupName: string, + itemName: string, + autoCloseOldConns: boolean, +) { return async (dispatch: DispatchFn, getState: GetStateFn) => { // switch proxy asynchronously - switchProxyImpl(dispatch, getState, apiConfig, groupName, itemName).catch(noop); + switchProxyImpl(dispatch, getState, apiConfig, groupName, itemName, autoCloseOldConns).catch( + noop, + ); // optimistic UI update dispatch('store/proxies#switchProxy', (s) => { @@ -298,13 +304,21 @@ function requestDelayForProxyOnce(apiConfig: ClashAPIConfig, name: string, laten }; } -export function requestDelayForProxy(apiConfig: ClashAPIConfig, name: string, latencyTestUrl: string) { +export function requestDelayForProxy( + apiConfig: ClashAPIConfig, + name: string, + latencyTestUrl: string, +) { return async (dispatch: DispatchFn) => { await dispatch(requestDelayForProxyOnce(apiConfig, name, latencyTestUrl)); }; } -export function requestDelayForProxies(apiConfig: ClashAPIConfig, names: string[], latencyTestUrl: string) { +export function requestDelayForProxies( + apiConfig: ClashAPIConfig, + names: string[], + latencyTestUrl: string, +) { return async (dispatch: DispatchFn, getState: GetStateFn) => { const proxies = getProxies(getState()); diff --git a/src/store/types.ts b/src/store/types.ts index 715b642df..ee963fe74 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,5 +1,6 @@ import type { ClashAPIConfig } from 'src/types'; +export type ThemeType = 'dark' | 'light' | 'auto'; export type ClashAPIConfigWithAddedAt = ClashAPIConfig & { addedAt?: number }; export type StateApp = { selectedClashAPIConfigIndex: number; @@ -7,7 +8,7 @@ export type StateApp = { latencyTestUrl: string; selectedChartStyleIndex: number; - theme: string; + theme: ThemeType; collapsibleIsOpen: Record; proxySortBy: string;