diff options
| author | yaling888 <[email protected]> | 2022-05-06 03:55:39 +0800 |
|---|---|---|
| committer | yaling888 <[email protected]> | 2022-05-06 03:55:39 +0800 |
| commit | dafd4486f17fcd72ac86578854886a807b0c4748 (patch) | |
| tree | d15d3345690f84000afda53c5c2f2ebe84cfdb8b /src | |
| parent | 96c16b0ae5562cbe16b311da0ed9f839da172c4e (diff) | |
| parent | 4dea888769ef153806bc5275616fd3c9d3e0a32b (diff) | |
Merge 'tracking' into master
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/logs.ts | 21 | ||||
| -rw-r--r-- | src/components/Connections.tsx | 39 | ||||
| -rw-r--r-- | src/components/Logs.module.scss | 9 | ||||
| -rw-r--r-- | src/components/Logs.tsx | 63 | ||||
| -rw-r--r-- | src/components/Root.scss | 26 | ||||
| -rw-r--r-- | src/components/SideBar.module.scss | 2 | ||||
| -rw-r--r-- | src/components/shared/Basic.module.scss | 46 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.module.css | 28 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.module.scss | 58 | ||||
| -rw-r--r-- | src/components/shared/ThemeSwitcher.tsx | 84 | ||||
| -rw-r--r-- | src/components/shared/rtf.css | 4 | ||||
| -rw-r--r-- | src/i18n/en.ts | 3 | ||||
| -rw-r--r-- | src/i18n/zh.ts | 3 | ||||
| -rw-r--r-- | src/store/app.ts | 52 | ||||
| -rw-r--r-- | src/store/types.ts | 1 |
15 files changed, 248 insertions, 191 deletions
diff --git a/src/api/logs.ts b/src/api/logs.ts index cb74bf3..044a31b 100644 --- a/src/api/logs.ts +++ b/src/api/logs.ts @@ -5,6 +5,12 @@ import { LogsAPIConfig } from 'src/types'; import { buildLogsWebSocketURL, getURLAndInit } from '../misc/request-helper'; type AppendLogFn = (x: Log) => void; +enum WebSocketReadyState { + Connecting = 0, + Open = 1, + Closing = 2, + Closed = 3, +} const endpoint = '/logs'; const textDecoder = new TextDecoder('utf-8'); @@ -86,20 +92,13 @@ function makeConnStr(c: LogsAPIConfig) { let prevConnStr: string; let controller: AbortController; -// 1 OPEN -// other value CLOSED -// similar to ws readyState but not the same -// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState -let wsState: number; export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { if (apiConfig.logLevel === 'uninit') return; - if (fetched || wsState === 1) return; + if (fetched || (ws && ws.readyState === WebSocketReadyState.Open)) return; prevAppendLogFn = appendLog; - wsState = 1; const url = buildLogsWebSocketURL(apiConfig, endpoint); ws = new WebSocket(url); ws.addEventListener('error', () => { - wsState = 3; fetchLogsWithFetch(apiConfig, appendLog); }); ws.addEventListener('message', function (event) { @@ -107,10 +106,14 @@ export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { }); } +export function stop() { + ws.close(); + if (controller) controller.abort(); +} + export function reconnect(apiConfig: LogsAPIConfig) { if (!prevAppendLogFn || !ws) return; ws.close(); - wsState = 3; fetched = false; fetchLogs(apiConfig, prevAppendLogFn); } diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index ff38004..f25e962 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -99,7 +99,7 @@ function formatConnectionDataItem( download, start: now - new Date(start).valueOf(), chains: chains.reverse().join(' / '), - rule: rule === 'GeoSite' || rule === 'GeoIP' || rule === 'RuleSet' ? `${rule} (${rulePayload})` : rule, + rule: (rulePayload == null || rulePayload === '') ? rule : (`${rule} (${rulePayload})`), ...metadata, host: `${host2}:${destinationPort}`, type: `${type}(${network})`, @@ -134,10 +134,7 @@ function Conn({ apiConfig }) { const filteredClosedConns = filterConns(closedConns, filterKeyword); const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false); const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []); - const closeCloseAllModal = useCallback( - () => setIsCloseAllModalOpen(false), - [] - ); + const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []); const [isRefreshPaused, setIsRefreshPaused] = useState(false); const toggleIsRefreshPaused = useCallback(() => { setIsRefreshPaused((x) => !x); @@ -165,11 +162,7 @@ function Conn({ apiConfig }) { }); // if previous connections and current connections are both empty // arrays, we wont update state to avaoid rerender - if ( - x && - (x.length !== 0 || prevConnsRef.current.length !== 0) && - !isRefreshPaused - ) { + if (x && (x.length !== 0 || prevConnsRef.current.length !== 0) && !isRefreshPaused) { prevConnsRef.current = x; setConns(x); } else { @@ -222,14 +215,9 @@ function Conn({ apiConfig }) { /> </div> </div> - <div - // @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message - ref={refContainer} - style={{ padding: 30, paddingBottom, paddingTop: 0 }} - > + <div ref={refContainer} style={{ padding: 30, paddingBottom, paddingTop: 0 }}> <div style={{ - // @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message height: containerHeight - paddingBottom, overflow: 'auto', }} @@ -237,24 +225,13 @@ function Conn({ apiConfig }) { <TabPanel> <>{renderTableOrPlaceholder(filteredConns)}</> <Fab - icon={ - isRefreshPaused ? <Play size={16} /> : <Pause size={16} /> - } - mainButtonStyles={ - isRefreshPaused - ? { - background: '#e74c3c', - } - : {} - } + icon={isRefreshPaused ? <Play size={16} /> : <Pause size={16} />} + mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}} style={fabPosition} - text={isRefreshPaused ? 'Resume Refresh' : 'Pause Refresh'} + text={isRefreshPaused ? t('Resume Refresh') : t('Pause Refresh')} onClick={toggleIsRefreshPaused} > - <Action - text="Close All Connections" - onClick={openCloseAllModal} - > + <Action text="Close All Connections" onClick={openCloseAllModal}> <IconClose size={10} /> </Action> </Fab> diff --git a/src/components/Logs.module.scss b/src/components/Logs.module.scss index 508e9c6..16ecb7f 100644 --- a/src/components/Logs.module.scss +++ b/src/components/Logs.module.scss @@ -41,19 +41,16 @@ color: var(--color-text); :global { - li { + .log { + padding: 10px 40px; background: var(--color-background); } - li.even { + .log.even { background: var(--color-background); } } } -.log { - padding: 10px 40px; -} - /*******************/ .logPlaceholder { diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index 019edd5..d9c53d7 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -1,23 +1,21 @@ import cx from 'clsx'; import * as React from 'react'; +import { Pause, Play } from 'react-feather'; import { useTranslation } from 'react-i18next'; -import { - areEqual, - FixedSizeList as List, - ListChildComponentProps, -} from 'react-window'; -import { fetchLogs } from 'src/api/logs'; +import { areEqual, FixedSizeList as List, ListChildComponentProps } from 'react-window'; +import { fetchLogs, reconnect as reconnectLogs,stop as stopLogs } from 'src/api/logs'; import ContentHeader from 'src/components/ContentHeader'; import LogSearch from 'src/components/LogSearch'; -import { connect } from 'src/components/StateProvider'; +import { connect, useStoreActions } from 'src/components/StateProvider'; import SvgYacd from 'src/components/SvgYacd'; import useRemainingViewPortHeight from 'src/hooks/useRemainingViewPortHeight'; -import { getClashAPIConfig } from 'src/store/app'; +import { getClashAPIConfig, getLogStreamingPaused } from 'src/store/app'; import { getLogLevel } from 'src/store/configs'; import { appendLog, getLogsForDisplay } from 'src/store/logs'; import { Log, State } from 'src/store/types'; import s from './Logs.module.scss'; +import { Fab, position as fabPosition } from './shared/Fab'; const { useCallback, memo, useEffect } = React; @@ -32,7 +30,7 @@ const colors = { type LogLineProps = Partial<Log>; function LogLine({ time, even, payload, type }: LogLineProps) { - const className = cx({ even }, s.log); + const className = cx({ even }, 'log'); return ( <div className={className}> <div className={s.logMeta}> @@ -51,23 +49,24 @@ function itemKey(index: number, data: LogLineProps[]) { return item.id; } -const Row = memo( - ({ index, style, data }: ListChildComponentProps<LogLineProps>) => { - const r = data[index]; - return ( - <div style={style}> - <LogLine {...r} /> - </div> - ); - }, - areEqual -); - -function Logs({ dispatch, logLevel, apiConfig, logs }) { - const appendLogInternal = useCallback( - (log) => dispatch(appendLog(log)), - [dispatch] +const Row = memo(({ index, style, data }: ListChildComponentProps<LogLineProps>) => { + const r = data[index]; + return ( + <div style={style}> + <LogLine {...r} /> + </div> ); +}, areEqual); + +function Logs({ dispatch, logLevel, apiConfig, logs, logStreamingPaused }) { + 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]); + const appendLogInternal = useCallback((log) => dispatch(appendLog(log)), [dispatch]); useEffect(() => { fetchLogs({ ...apiConfig, logLevel }, appendLogInternal); }, [apiConfig, logLevel, appendLogInternal]); @@ -80,10 +79,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) { <LogSearch /> <div ref={refLogsContainer} style={{ paddingBottom }}> {logs.length === 0 ? ( - <div - className={s.logPlaceholder} - style={{ height: containerHeight - paddingBottom }} - > + <div className={s.logPlaceholder} style={{ height: containerHeight - paddingBottom }}> <div className={s.logPlaceholderIcon}> <SvgYacd width={200} height={200} /> </div> @@ -101,6 +97,14 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) { > {Row} </List> + + <Fab + icon={logStreamingPaused ? <Play size={16} /> : <Pause size={16} />} + mainButtonStyles={logStreamingPaused ? { background: '#e74c3c' } : {}} + style={fabPosition} + text={logStreamingPaused ? t('Resume Refresh') : t('Pause Refresh')} + onClick={toggleIsRefreshPaused} + ></Fab> </div> )} </div> @@ -112,6 +116,7 @@ const mapState = (s: State) => ({ logs: getLogsForDisplay(s), logLevel: getLogLevel(s), apiConfig: getClashAPIConfig(s), + logStreamingPaused: getLogStreamingPaused(s), }); export default connect(mapState)(Logs); diff --git a/src/components/Root.scss b/src/components/Root.scss index 55198ab..4ae7d5f 100644 --- a/src/components/Root.scss +++ b/src/components/Root.scss @@ -53,18 +53,14 @@ :root { --font-mono: 'Roboto Mono', Menlo, monospace; - --font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, - Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, - 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; + // prettier-ignore + --font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; --color-focus-blue: #1a73e8; --btn-bg: #387cec; } body { - font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, - Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, - Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, - sans-serif; + font-family: var(--font-normal); -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; @@ -128,17 +124,25 @@ body { --select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20); } -// we don't have a "system" or "auto" mode now -// it's just not make sense to have these yet -// @media (prefers-color-scheme: dark) {} -// @media (prefers-color-scheme: light) {} +:root[data-theme='auto'] { + @media (prefers-color-scheme: dark) { + @include dark; + color-scheme: dark; + } + @media (prefers-color-scheme: light) { + @include light; + color-scheme: light; + } +} :root[data-theme='dark'] { @include dark; + color-scheme: dark; } :root[data-theme='light'] { @include light; + color-scheme: light; } .flexCenter { diff --git a/src/components/SideBar.module.scss b/src/components/SideBar.module.scss index 4a06377..4bd2e60 100644 --- a/src/components/SideBar.module.scss +++ b/src/components/SideBar.module.scss @@ -15,7 +15,7 @@ @media (max-width: 768px) { display: flex; justify-content: space-between; - overflow: scroll; + overflow: auto; } } diff --git a/src/components/shared/Basic.module.scss b/src/components/shared/Basic.module.scss index 8e5a113..b8e0068 100644 --- a/src/components/shared/Basic.module.scss +++ b/src/components/shared/Basic.module.scss @@ -36,32 +36,30 @@ h2.sectionNameType { * */ -:global { - body.light { - /* +:root[data-theme='light'] { + /* * --loading-dot-{dot-index}-{dot-keyframe-phase} */ - --loading-dot-1-1: rgba(0, 0, 0, 0.1); - --loading-dot-1-2: rgba(0, 0, 0, 0.5); - --loading-dot-1-3: rgba(0, 0, 0, 0.3); - --loading-dot-2-1: rgba(0, 0, 0, 0.3); - --loading-dot-2-2: rgba(0, 0, 0, 0.1); - --loading-dot-2-3: rgba(0, 0, 0, 0.5); - --loading-dot-3-1: rgba(0, 0, 0, 0.5); - --loading-dot-3-2: rgba(0, 0, 0, 0.3); - --loading-dot-3-3: rgba(0, 0, 0, 0.1); - } - body.dark { - --loading-dot-1-1: rgba(255, 255, 255, 0.5); - --loading-dot-1-2: rgba(255, 255, 255, 0.1); - --loading-dot-1-3: rgba(255, 255, 255, 0.3); - --loading-dot-2-1: rgba(255, 255, 255, 0.3); - --loading-dot-2-2: rgba(255, 255, 255, 0.5); - --loading-dot-2-3: rgba(255, 255, 255, 0.1); - --loading-dot-3-1: rgba(255, 255, 255, 0.1); - --loading-dot-3-2: rgba(255, 255, 255, 0.3); - --loading-dot-3-3: rgba(255, 255, 255, 0.5); - } + --loading-dot-1-1: rgba(0, 0, 0, 0.1); + --loading-dot-1-2: rgba(0, 0, 0, 0.5); + --loading-dot-1-3: rgba(0, 0, 0, 0.3); + --loading-dot-2-1: rgba(0, 0, 0, 0.3); + --loading-dot-2-2: rgba(0, 0, 0, 0.1); + --loading-dot-2-3: rgba(0, 0, 0, 0.5); + --loading-dot-3-1: rgba(0, 0, 0, 0.5); + --loading-dot-3-2: rgba(0, 0, 0, 0.3); + --loading-dot-3-3: rgba(0, 0, 0, 0.1); +} +:root[data-theme='dark'] { + --loading-dot-1-1: rgba(255, 255, 255, 0.5); + --loading-dot-1-2: rgba(255, 255, 255, 0.1); + --loading-dot-1-3: rgba(255, 255, 255, 0.3); + --loading-dot-2-1: rgba(255, 255, 255, 0.3); + --loading-dot-2-2: rgba(255, 255, 255, 0.5); + --loading-dot-2-3: rgba(255, 255, 255, 0.1); + --loading-dot-3-1: rgba(255, 255, 255, 0.1); + --loading-dot-3-2: rgba(255, 255, 255, 0.3); + --loading-dot-3-3: rgba(255, 255, 255, 0.5); } .loadingDot, diff --git a/src/components/shared/ThemeSwitcher.module.css b/src/components/shared/ThemeSwitcher.module.css deleted file mode 100644 index 919c86c..0000000 --- a/src/components/shared/ThemeSwitcher.module.css +++ /dev/null @@ -1,28 +0,0 @@ -.iconWrapper { - --sz: 40px; - - width: var(--sz); - height: var(--sz); - display: flex; - justify-content: center; - align-items: center; - - outline: none; - padding: 5px; - color: var(--color-text); - border-radius: 100%; - border: 1px solid transparent; -} -.iconWrapper:hover { - opacity: 0.6; -} -.iconWrapper:focus { - border-color: var(--color-focus-blue); -} - -.themeSwitchContainer { - appearance: none; - user-select: none; - background: none; - cursor: pointer; -} diff --git a/src/components/shared/ThemeSwitcher.module.scss b/src/components/shared/ThemeSwitcher.module.scss new file mode 100644 index 0000000..c5de126 --- /dev/null +++ b/src/components/shared/ThemeSwitcher.module.scss @@ -0,0 +1,58 @@ +.iconWrapper { + --sz: 40px; + + width: var(--sz); + height: var(--sz); + display: flex; + justify-content: center; + align-items: center; + + outline: none; + padding: 5px; + color: var(--color-text); + border-radius: 100%; + border: 1px solid transparent; +} +.iconWrapper:hover { + opacity: 0.6; +} +.iconWrapper:focus { + border-color: var(--color-focus-blue); +} + +.themeSwitchContainer { + --sz: 40px; + + position: relative; + display: flex; + align-items: center; + height: var(--sz); + select { + cursor: pointer; + padding-left: var(--sz); + width: var(--sz); + height: var(--sz); + appearance: none; + outline: none; + border-radius: 100%; + border: 1px solid transparent; + background: var(--color-bg-sidebar); + &:focus { + border-color: var(--color-focus-blue); + } + option { + // this has effect in Firefox + // Chrome and Safari use the native menu + background: var(--color-bg-sidebar); + } + } + .iconWrapper { + pointer-events: none; + display: inline-flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + top: 0; + } +} diff --git a/src/components/shared/ThemeSwitcher.tsx b/src/components/shared/ThemeSwitcher.tsx index fba5b0b..45b60bc 100644 --- a/src/components/shared/ThemeSwitcher.tsx +++ b/src/components/shared/ThemeSwitcher.tsx @@ -1,5 +1,4 @@ import Tooltip from '@reach/tooltip'; -import cx from 'clsx'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { connect } from 'src/components/StateProvider'; @@ -7,28 +6,40 @@ import { framerMotionResouce } from 'src/misc/motion'; import { getTheme, switchTheme } from 'src/store/app'; import { State } from 'src/store/types'; -import s from './ThemeSwitcher.module.css'; +import s from './ThemeSwitcher.module.scss'; export function ThemeSwitcherImpl({ theme, dispatch }) { const { t } = useTranslation(); - const switchThemeHooked = React.useCallback(() => { - dispatch(switchTheme()); - }, [dispatch]); + const themeIcon = React.useMemo(() => { + switch (theme) { + case 'dark': + return <MoonA />; + case 'auto': + return <Auto />; + case 'light': + return <Sun />; + default: + console.assert(false, 'Unknown theme'); + return <MoonA />; + } + }, [theme]); + + const onChange = React.useCallback( + (e: React.ChangeEvent<HTMLSelectElement>) => dispatch(switchTheme(e.target.value)), + [dispatch] + ); return ( - <Tooltip - label={t('theme')} - aria-label={ - 'switch to ' + (theme === 'light' ? 'dark' : 'light') + ' theme' - } - > - <button - className={cx(s.iconWrapper, s.themeSwitchContainer)} - onClick={switchThemeHooked} - > - {theme === 'light' ? <MoonA /> : <Sun />} - </button> + <Tooltip label={t('switch_theme')} aria-label={'switch theme'}> + <div className={s.themeSwitchContainer}> + <span className={s.iconWrapper}>{themeIcon}</span> + <select onChange={onChange}> + <option value="auto">Auto</option> + <option value="dark">Dark</option> + <option value="light">Light</option> + </select> + </div> </Tooltip> ); } @@ -75,11 +86,7 @@ function Sun() { strokeLinejoin="round" > <circle cx="12" cy="12" r="5"></circle> - <motion.g - initial={{ scale: 0.8 }} - animate={{ scale: 1 }} - transition={{ duration: 0.7 }} - > + <motion.g initial={{ scale: 0.7 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }}> <line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> @@ -93,5 +100,38 @@ function Sun() { ); } +function Auto() { + const module = framerMotionResouce.read(); + const motion = module.motion; + + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <circle cx="12" cy="12" r="11" /> + <clipPath id="cut-off-bottom"> + <motion.rect + x="12" + y="0" + width="12" + height="24" + initial={{ rotate: -30 }} + animate={{ rotate: 0 }} + transition={{ duration: 0.7 }} + /> + </clipPath> + <circle cx="12" cy="12" r="6" clipPath="url(#cut-off-bottom)" fill="currentColor" /> + </svg> + ); +} + const mapState = (s: State) => ({ theme: getTheme(s) }); export const ThemeSwitcher = connect(mapState)(ThemeSwitcherImpl); diff --git a/src/components/shared/rtf.css b/src/components/shared/rtf.css index da439ee..574aad1 100644 --- a/src/components/shared/rtf.css +++ b/src/components/shared/rtf.css @@ -12,8 +12,8 @@ list-style: none; } .rtf.open .rtf--mb { - box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), - 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); + box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), + 0px 3px 14px 2px rgba(0, 0, 0, 0.12); } .rtf.open .rtf--mb > ul { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ac0aa6c..fcdf253 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -10,6 +10,8 @@ export const data = { 'Upload Total': 'Upload Total', 'Download Total': 'Download Total', 'Active Connections': 'Active Connections', + 'Pause Refresh': 'Pause Refresh', + 'Resume Refresh': 'Resume Refresh', Up: 'Up', Down: 'Down', 'Test Latency': 'Test Latency', @@ -25,6 +27,7 @@ export const data = { Connections: 'Connections', Active: 'Active', Closed: 'Closed', + switch_theme: 'Switch theme', theme: 'theme', about: 'about', no_logs: 'No logs yet, hang tight...', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index a8f4c05..e92f9ff 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -10,6 +10,8 @@ export const data = { 'Upload Total': '上传总量', 'Download Total': '下载总量', 'Active Connections': '活动连接', + 'Pause Refresh': '暂停刷新', + 'Resume Refresh': '继续刷新', Up: '上传', Down: '下载', 'Test Latency': '延迟测速', @@ -25,6 +27,7 @@ export const data = { Connections: '连接', Active: '活动', Closed: '已断开', + switch_theme: '切换主题', theme: '主题', about: '关于', no_logs: '暂无日志...', diff --git a/src/store/app.ts b/src/store/app.ts index c6a455e..6680e6e 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -9,18 +9,16 @@ export const getClashAPIConfig = (s: State) => { const idx = s.app.selectedClashAPIConfigIndex; return s.app.clashAPIConfigs[idx]; }; -export const getSelectedClashAPIConfigIndex = (s: State) => - s.app.selectedClashAPIConfigIndex; +export const getSelectedClashAPIConfigIndex = (s: State) => s.app.selectedClashAPIConfigIndex; export const getClashAPIConfigs = (s: State) => s.app.clashAPIConfigs; export const getTheme = (s: State) => s.app.theme; -export const getSelectedChartStyleIndex = (s: State) => - s.app.selectedChartStyleIndex; +export const getSelectedChartStyleIndex = (s: State) => s.app.selectedChartStyleIndex; export const getLatencyTestUrl = (s: State) => s.app.latencyTestUrl; export const getCollapsibleIsOpen = (s: State) => s.app.collapsibleIsOpen; export const getProxySortBy = (s: State) => s.app.proxySortBy; -export const getHideUnavailableProxies = (s: State) => - s.app.hideUnavailableProxies; +export const getHideUnavailableProxies = (s: State) => s.app.hideUnavailableProxies; export const getAutoCloseOldConns = (s: State) => s.app.autoCloseOldConns; +export const getLogStreamingPaused = (s: State) => s.app.logStreamingPaused; const saveStateDebounced = debounce(saveState, 600); @@ -96,34 +94,33 @@ export function updateClashAPIConfig({ baseURL, secret }) { } const rootEl = document.querySelector('html'); -const themeColorMeta = document.querySelector('meta[name="theme-color"]'); -function setTheme(theme = 'dark') { - if (theme === 'dark') { +type ThemeType = 'dark' | 'light' | 'auto'; + +function setTheme(theme: ThemeType = 'dark') { + if (theme === 'auto') { + rootEl.setAttribute('data-theme', 'auto'); + } else if (theme === 'dark') { rootEl.setAttribute('data-theme', 'dark'); - themeColorMeta.setAttribute('content', '#202020'); } else { rootEl.setAttribute('data-theme', 'light'); - themeColorMeta.setAttribute('content', '#eeeeee'); } } -export function switchTheme() { +export function switchTheme(nextTheme = 'auto') { return (dispatch: DispatchFn, getState: GetStateFn) => { const currentTheme = getTheme(getState()); - const theme = currentTheme === 'light' ? 'dark' : 'light'; + if (currentTheme === nextTheme) return; // side effect - setTheme(theme); + setTheme(nextTheme as ThemeType); dispatch('storeSwitchTheme', (s) => { - s.app.theme = theme; + s.app.theme = nextTheme; }); // side effect saveState(getState().app); }; } -export function selectChartStyleIndex( - selectedChartStyleIndex: number | string -) { +export function selectChartStyleIndex(selectedChartStyleIndex: number | string) { return (dispatch: DispatchFn, getState: GetStateFn) => { dispatch('appSelectChartStyleIndex', (s) => { s.app.selectedChartStyleIndex = Number(selectedChartStyleIndex); @@ -143,11 +140,7 @@ export function updateAppConfig(name: string, value: unknown) { }; } -export function updateCollapsibleIsOpen( - prefix: string, - name: string, - v: boolean -) { +export function updateCollapsibleIsOpen(prefix: string, name: string, v: boolean) { return (dispatch: DispatchFn, getState: GetStateFn) => { dispatch('updateCollapsibleIsOpen', (s: State) => { s.app.collapsibleIsOpen[`${prefix}:${name}`] = v; @@ -158,11 +151,9 @@ export function updateCollapsibleIsOpen( } const defaultClashAPIConfig = { - baseURL: - document.getElementById('app')?.getAttribute('data-base-url') ?? - 'http://127.0.0.1:9090', + baseURL: document.getElementById('app')?.getAttribute('data-base-url') ?? 'http://127.0.0.1:9090', secret: '', - addedAt: 0, + addedAt: 0 }; // type Theme = 'light' | 'dark'; const defaultState: StateApp = { @@ -179,6 +170,7 @@ const defaultState: StateApp = { proxySortBy: 'Natural', hideUnavailableProxies: false, autoCloseOldConns: false, + logStreamingPaused: false }; function parseConfigQueryString() { @@ -202,7 +194,11 @@ export function initialState() { if (conf) { const url = new URL(conf.baseURL); if (query.hostname) { - url.hostname = query.hostname; + if (query.hostname.indexOf('http') === 0) { + url.href = decodeURIComponent(query.hostname); + } else { + url.hostname = query.hostname; + } } if (query.port) { url.port = query.port; diff --git a/src/store/types.ts b/src/store/types.ts index 7e6a39d..b9141ac 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -13,6 +13,7 @@ export type StateApp = { proxySortBy: string; hideUnavailableProxies: boolean; autoCloseOldConns: boolean; + logStreamingPaused: boolean; }; export type ClashGeneralConfig = { |
