summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoryaling888 <[email protected]>2022-05-06 03:55:39 +0800
committeryaling888 <[email protected]>2022-05-06 03:55:39 +0800
commitdafd4486f17fcd72ac86578854886a807b0c4748 (patch)
treed15d3345690f84000afda53c5c2f2ebe84cfdb8b /src
parent96c16b0ae5562cbe16b311da0ed9f839da172c4e (diff)
parent4dea888769ef153806bc5275616fd3c9d3e0a32b (diff)
Merge 'tracking' into master
Diffstat (limited to 'src')
-rw-r--r--src/api/logs.ts21
-rw-r--r--src/components/Connections.tsx39
-rw-r--r--src/components/Logs.module.scss9
-rw-r--r--src/components/Logs.tsx63
-rw-r--r--src/components/Root.scss26
-rw-r--r--src/components/SideBar.module.scss2
-rw-r--r--src/components/shared/Basic.module.scss46
-rw-r--r--src/components/shared/ThemeSwitcher.module.css28
-rw-r--r--src/components/shared/ThemeSwitcher.module.scss58
-rw-r--r--src/components/shared/ThemeSwitcher.tsx84
-rw-r--r--src/components/shared/rtf.css4
-rw-r--r--src/i18n/en.ts3
-rw-r--r--src/i18n/zh.ts3
-rw-r--r--src/store/app.ts52
-rw-r--r--src/store/types.ts1
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 = {