update to 0.3.6
This commit is contained in:
parent
5d67349046
commit
e33fb875dd
38 changed files with 6010 additions and 2804 deletions
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
30
.github/workflows/push.yml
vendored
Normal file
30
.github/workflows/push.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: Deploy
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Dashboard code
|
||||
uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: pnpm
|
||||
- name: Install package and build
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build
|
||||
- name: Deploy
|
||||
uses: crazy-max/ghaction-github-pages@v2
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: public
|
||||
fqdn: yacd.metacubex.one
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
public
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
|
@ -1,5 +1,7 @@
|
|||
[中文](./README_CN.md)
|
||||
|
||||
<h1 align="center">
|
||||
<img src="https://user-images.githubusercontent.com/1166872/47954055-97e6cb80-dfc0-11e8-991f-230fd40481e5.png" alt="yacd">
|
||||
<img src="https://user-images.githubusercontent.com/78135608/232244383-5e1389db-ce56-4c83-9627-4f3d1a489c6e.png" alt="yacd">
|
||||
</h1>
|
||||
|
||||
> Yet Another [Clash](https://github.com/yaling888/clash) [Dashboard](https://github.com/yaling888/clash-dashboard)
|
||||
|
@ -8,8 +10,7 @@
|
|||
|
||||
Install [twemoji](https://github.com/mozilla/twemoji-colr/releases) to display emoji better on Windows system.
|
||||
|
||||
|
||||
The site http://yacd.metacubex.one is served with HTTP not HTTPS is because many browsers block requests to HTTP resources from a HTTPS website. If you think it's not safe, you could just download the [zip of the gh-pages](https://github.com/yaling888/yacd/archive/gh-pages.zip), unzip and serve those static files with a web server(like Nginx).
|
||||
The site http://yacd.metacubex.one is served with HTTP not HTTPS is because many browsers block requests to HTTP resources from a HTTPS website. If you think it's not safe, you could just download the [zip of the gh-pages](https://github.com/MetaCubeX/yacd/archive/gh-pages.zip), unzip and serve those static files with a web server(like Nginx).
|
||||
|
||||
**Supported URL query params**
|
||||
|
||||
|
|
36
README_CN.md
Normal file
36
README_CN.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
<h1 align="center">
|
||||
<img src="https://user-images.githubusercontent.com/78135608/232244383-5e1389db-ce56-4c83-9627-4f3d1a489c6e.png" alt="yacd">
|
||||
</h1>
|
||||
|
||||
> Yet Another [Clash](https://github.com/yaling888/clash) [Dashboard](https://github.com/yaling888/clash-dashboard)
|
||||
|
||||
## 用法
|
||||
|
||||
安装[twemoji](https://github.com/mozilla/twemoji-colr/releases)以在 Windows 系统上更好地显示表情符号。
|
||||
|
||||
网站 http://yacd.metacubex.one 是通过 HTTP 提供服务的,而不是 HTTPS,因为许多浏览器会阻止从 HTTPS 网站请求 HTTP 资源。如果认为这不安全,可以下载[gh-pages 的 zip 文件](https://github.com/MetaCubeX/yacd/archive/gh-pages.zip),解压缩并使用 Web 服务器(如 Nginx)提供这些静态文件。
|
||||
|
||||
**支持的 URL 查询参数**
|
||||
|
||||
| 参数 | 描述 |
|
||||
| -------- | ------------------------------------------------------------------ |
|
||||
| hostname | Clash 后端 API 的主机名(通常是`external-controller`的 host 部分) |
|
||||
| port | Clash 后端 API 的端口号(通常是`external-controller`的 port 部分) |
|
||||
| secret | Clash API 密钥(`config.yaml`中的"secret") |
|
||||
| theme | UI 颜色方案(dark、light、auto) |
|
||||
|
||||
## 开发部署
|
||||
|
||||
```sh
|
||||
# 安装依赖库
|
||||
# 你可以使用 `npm i -g pnpm` 安装 pnpm
|
||||
pnpm i
|
||||
|
||||
# 启动开发服务器
|
||||
# 然后访问 http://127.0.0.1:3000
|
||||
pnpm start
|
||||
|
||||
# 构建优化资源
|
||||
# 准备好部署的资源将在目录 `public` 中
|
||||
pnpm build
|
||||
```
|
BIN
assets/Twemoji_Mozilla.ttf
Normal file
BIN
assets/Twemoji_Mozilla.ttf
Normal file
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "yacd",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"description": "Yet another Clash dashboard",
|
||||
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
|
||||
"author": "MetaCubeX (https://github.com/MetaCubeX)",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
|
|
8043
pnpm-lock.yaml
8043
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,8 @@ import { ClashAPIConfig } from '~/types';
|
|||
const endpoint = '/configs';
|
||||
const updateGeoDatabasesFileEndpoint = '/configs/geo';
|
||||
const flushFakeIPPoolEndpoint = '/cache/fakeip/flush';
|
||||
const restartCoreEndpoint = '/restart';
|
||||
const upgradeCoreEndpoint = '/upgrade';
|
||||
|
||||
export async function fetchConfigs(apiConfig: ClashAPIConfig) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
|
@ -42,6 +44,18 @@ export async function updateGeoDatabasesFile(apiConfig: ClashAPIConfig) {
|
|||
return await fetch(url + updateGeoDatabasesFileEndpoint, { ...init, body, method: 'POST' });
|
||||
}
|
||||
|
||||
export async function restartCore(apiConfig: ClashAPIConfig) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
const body = '{"path": "", "payload": ""}';
|
||||
return await fetch(url + restartCoreEndpoint, { ...init, body, method: 'POST' });
|
||||
}
|
||||
|
||||
export async function upgradeCore(apiConfig: ClashAPIConfig) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
const body = '{"path": "", "payload": ""}';
|
||||
return await fetch(url + upgradeCoreEndpoint, { ...init, body, method: 'POST' });
|
||||
}
|
||||
|
||||
export async function flushFakeIPPool(apiConfig: ClashAPIConfig) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
return await fetch(url + flushFakeIPPoolEndpoint, { ...init, method: 'POST' });
|
||||
|
|
|
@ -5,6 +5,12 @@ import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
|
|||
const endpoint = '/connections';
|
||||
|
||||
const fetched = false;
|
||||
|
||||
interface Subscriber {
|
||||
listner: unknown; // on data received, listener will be called with data
|
||||
onClose: () => void; // on stream closed, onClose will be called
|
||||
}
|
||||
|
||||
const subscribers = [];
|
||||
|
||||
// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41
|
||||
|
@ -48,28 +54,49 @@ function appendData(s: string) {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('JSON.parse error', JSON.parse(s));
|
||||
}
|
||||
subscribers.forEach((f) => f(o));
|
||||
subscribers.forEach((s) => s.listner(o));
|
||||
}
|
||||
|
||||
type UnsubscribeFn = () => void;
|
||||
|
||||
let wsState: number;
|
||||
export function fetchData(apiConfig: ClashAPIConfig, listener: unknown): UnsubscribeFn | void {
|
||||
export function fetchData(
|
||||
apiConfig: ClashAPIConfig,
|
||||
listener: unknown,
|
||||
onClose: () => void
|
||||
): UnsubscribeFn | void {
|
||||
if (fetched || wsState === 1) {
|
||||
if (listener) return subscribe(listener);
|
||||
if (listener)
|
||||
return subscribe({
|
||||
listner: listener,
|
||||
onClose,
|
||||
});
|
||||
}
|
||||
wsState = 1;
|
||||
const url = buildWebSocketURL(apiConfig, endpoint);
|
||||
const ws = new WebSocket(url);
|
||||
ws.addEventListener('error', () => (wsState = 3));
|
||||
ws.addEventListener('error', () => {
|
||||
wsState = 3;
|
||||
subscribers.forEach((s) => s.onClose());
|
||||
subscribers.length = 0;
|
||||
});
|
||||
ws.addEventListener('close', () => {
|
||||
wsState = 3;
|
||||
subscribers.forEach((s) => s.onClose());
|
||||
subscribers.length = 0;
|
||||
});
|
||||
ws.addEventListener('message', (event) => appendData(event.data));
|
||||
if (listener) return subscribe(listener);
|
||||
if (listener)
|
||||
return subscribe({
|
||||
listner: listener,
|
||||
onClose,
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(listener: unknown): UnsubscribeFn {
|
||||
subscribers.push(listener);
|
||||
function subscribe(subscriber: Subscriber): UnsubscribeFn {
|
||||
subscribers.push(subscriber);
|
||||
return function unsubscribe() {
|
||||
const idx = subscribers.indexOf(listener);
|
||||
const idx = subscribers.indexOf(subscriber);
|
||||
subscribers.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
|
119
src/api/memory.ts
Normal file
119
src/api/memory.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { ClashAPIConfig } from '~/types';
|
||||
|
||||
import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
|
||||
|
||||
const endpoint = '/memory';
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
|
||||
const Size = 150;
|
||||
|
||||
const memory = {
|
||||
labels: Array(Size).fill(0),
|
||||
inuse: Array(Size),
|
||||
oslimit: Array(Size),
|
||||
|
||||
size: Size,
|
||||
subscribers: [],
|
||||
appendData(o: { inuse: number; oslimit: number }) {
|
||||
this.inuse.shift();
|
||||
this.oslimit.shift();
|
||||
this.labels.shift();
|
||||
|
||||
const l = Date.now();
|
||||
this.inuse.push(o.inuse);
|
||||
this.oslimit.push(o.oslimit);
|
||||
this.labels.push(l);
|
||||
|
||||
this.subscribers.forEach((f) => f(o));
|
||||
},
|
||||
|
||||
subscribe(listener: (x: any) => void) {
|
||||
this.subscribers.push(listener);
|
||||
return () => {
|
||||
const idx = this.subscribers.indexOf(listener);
|
||||
this.subscribers.splice(idx, 1);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
let fetched = false;
|
||||
let decoded = '';
|
||||
|
||||
function parseAndAppend(x: string) {
|
||||
memory.appendData(JSON.parse(x));
|
||||
}
|
||||
|
||||
function pump(reader: ReadableStreamDefaultReader) {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
const str = textDecoder.decode(value, { stream: !done });
|
||||
decoded += str;
|
||||
|
||||
const splits = decoded.split('\n');
|
||||
|
||||
const lastSplit = splits[splits.length - 1];
|
||||
|
||||
for (let i = 0; i < splits.length - 1; i++) {
|
||||
parseAndAppend(splits[i]);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
parseAndAppend(lastSplit);
|
||||
decoded = '';
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GET /memory streaming done');
|
||||
fetched = false;
|
||||
return;
|
||||
} else {
|
||||
decoded = lastSplit;
|
||||
}
|
||||
return pump(reader);
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
function fetchData(apiConfig: ClashAPIConfig) {
|
||||
if (fetched || wsState === 1) return memory;
|
||||
wsState = 1;
|
||||
const url = buildWebSocketURL(apiConfig, endpoint);
|
||||
const ws = new WebSocket(url);
|
||||
ws.addEventListener('error', function (_ev) {
|
||||
wsState = 3;
|
||||
});
|
||||
ws.addEventListener('close', function (_ev) {
|
||||
wsState = 3;
|
||||
fetchDataWithFetch(apiConfig);
|
||||
});
|
||||
ws.addEventListener('message', function (event) {
|
||||
parseAndAppend(event.data);
|
||||
});
|
||||
return memory;
|
||||
}
|
||||
|
||||
function fetchDataWithFetch(apiConfig: ClashAPIConfig) {
|
||||
if (fetched) return memory;
|
||||
fetched = true;
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
fetch(url + endpoint, init).then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
const reader = response.body.getReader();
|
||||
pump(reader);
|
||||
} else {
|
||||
fetched = false;
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('fetch /memory error', err);
|
||||
fetched = false;
|
||||
}
|
||||
);
|
||||
return memory;
|
||||
}
|
||||
|
||||
export { fetchData };
|
|
@ -34,7 +34,7 @@ export async function requestToSwitchProxy(apiConfig, name1, name2) {
|
|||
export async function requestDelayForProxy(
|
||||
apiConfig,
|
||||
name,
|
||||
latencyTestUrl = 'http://www.gstatic.com/generate_204'
|
||||
latencyTestUrl = 'https://www.gstatic.com/generate_204'
|
||||
) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
const qs = `timeout=5000&url=${encodeURIComponent(latencyTestUrl)}`;
|
||||
|
|
|
@ -15,8 +15,10 @@ import {
|
|||
flushFakeIPPool,
|
||||
getConfigs,
|
||||
reloadConfigFile,
|
||||
restartCore,
|
||||
updateConfigs,
|
||||
updateGeoDatabasesFile,
|
||||
upgradeCore,
|
||||
} from '../store/configs';
|
||||
import { openModal } from '../store/modals';
|
||||
import Button from './Button';
|
||||
|
@ -206,6 +208,14 @@ function ConfigImpl({
|
|||
dispatch(reloadConfigFile(apiConfig));
|
||||
}, [apiConfig, dispatch]);
|
||||
|
||||
const handleRestartCore = useCallback(() => {
|
||||
dispatch(restartCore(apiConfig));
|
||||
}, [apiConfig, dispatch]);
|
||||
|
||||
const handleUpgradeCore = useCallback(() => {
|
||||
dispatch(upgradeCore(apiConfig));
|
||||
}, [apiConfig, dispatch]);
|
||||
|
||||
const handleUpdateGeoDatabasesFile = useCallback(() => {
|
||||
dispatch(updateGeoDatabasesFile(apiConfig));
|
||||
}, [apiConfig, dispatch]);
|
||||
|
@ -351,6 +361,22 @@ function ConfigImpl({
|
|||
onClick={handleFlushFakeIPPool}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>Restart</div>
|
||||
<Button
|
||||
start={<RotateCw size={16} />}
|
||||
label={t('restart_core')}
|
||||
onClick={handleRestartCore}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>⚠️ Upgrade ⚠️</div>
|
||||
<Button
|
||||
start={<RotateCw size={16} />}
|
||||
label={t('upgrade_core')}
|
||||
onClick={handleUpgradeCore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s0.sep}>
|
||||
<div />
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
}
|
||||
|
||||
.th {
|
||||
padding: 8px 10px;
|
||||
padding: 3% 2px;
|
||||
height: 50px;
|
||||
background: var(--color-background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
user-select: none;
|
||||
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
@ -23,10 +23,42 @@
|
|||
}
|
||||
}
|
||||
|
||||
.thdu {
|
||||
padding: 3% 2px;
|
||||
height: 50px;
|
||||
background: var(--color-background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
font-size: 1em;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.break {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 8px 13px;
|
||||
/* 上边下边 | 左边右边 */
|
||||
padding: 10px 2px;
|
||||
font-size: 0.9em;
|
||||
// max-width: 14em;
|
||||
min-width: 9em;
|
||||
cursor: default;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
&:hover {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
@ -38,15 +70,16 @@
|
|||
}
|
||||
|
||||
/* download upload td cells */
|
||||
.du {
|
||||
text-align: right;
|
||||
.center {
|
||||
min-width: 7em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sortIconContainer {
|
||||
display: inline-flex;
|
||||
margin-left: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 0.5em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.rotate180 {
|
||||
|
|
|
@ -13,19 +13,19 @@ const sortDescFirst = true;
|
|||
|
||||
const columns = [
|
||||
{ accessor: 'id', show: false },
|
||||
{ Header: 'c_host', accessor: 'host' },
|
||||
{ Header: 'c_sni', accessor: 'sniffHost' },
|
||||
{ Header: 'c_type', accessor: 'type' },
|
||||
{ Header: 'c_process', accessor: 'process' },
|
||||
{ Header: 'c_dl', accessor: 'download', sortDescFirst },
|
||||
{ Header: 'c_ul', accessor: 'upload', sortDescFirst },
|
||||
{ Header: 'c_host', accessor: 'host' },
|
||||
{ Header: 'c_rule', accessor: 'rule' },
|
||||
{ Header: 'c_chains', accessor: 'chains' },
|
||||
{ Header: 'c_time', accessor: 'start' },
|
||||
{ Header: 'c_dl_speed', accessor: 'downloadSpeedCurr', sortDescFirst },
|
||||
{ Header: 'c_ul_speed', accessor: 'uploadSpeedCurr', sortDescFirst },
|
||||
{ Header: 'c_chains', accessor: 'chains' },
|
||||
{ Header: 'c_rule', accessor: 'rule' },
|
||||
{ Header: 'c_time', accessor: 'start', sortDescFirst },
|
||||
{ Header: 'c_dl', accessor: 'download', sortDescFirst },
|
||||
{ Header: 'c_ul', accessor: 'upload', sortDescFirst },
|
||||
{ Header: 'c_source', accessor: 'source' },
|
||||
{ Header: 'c_destination_ip', accessor: 'destinationIP' },
|
||||
{ Header: 'c_type', accessor: 'type' },
|
||||
{ Header: 'c_sni', accessor: 'sniffHost' },
|
||||
];
|
||||
|
||||
function renderCell(cell: { column: { id: string }; value: number }, locale: Locale) {
|
||||
|
@ -71,8 +71,11 @@ function Table({ data }) {
|
|||
{headerGroups.map((headerGroup) => {
|
||||
return (
|
||||
<div {...headerGroup.getHeaderGroupProps()} className={s.tr}>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<div {...column.getHeaderProps(column.getSortByToggleProps())} className={s.th}>
|
||||
{headerGroup.headers.map((column, index) => (
|
||||
<div
|
||||
{...column.getHeaderProps(column.getSortByToggleProps())}
|
||||
className={index == 0 || (index >= 5 && index < 10) ? s.thdu : s.th}
|
||||
>
|
||||
<span>{t(column.render('Header'))}</span>
|
||||
<span className={s.sortIconContainer}>
|
||||
{column.isSorted ? (
|
||||
|
@ -93,7 +96,8 @@ function Table({ data }) {
|
|||
className={cx(
|
||||
s.td,
|
||||
i % 2 === 0 ? s.odd : false,
|
||||
j >= 2 && j <= 5 ? s.du : false
|
||||
j == 0 || (j >= 5 && j < 10) ? s.center : true
|
||||
// j ==1 ? s.break : true
|
||||
)}
|
||||
>
|
||||
{renderCell(cell, locale)}
|
||||
|
|
|
@ -47,3 +47,7 @@
|
|||
.react-tabs__tab-panel--selected {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._btn_lzu00_1 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { State } from '~/store/types';
|
|||
import * as connAPI from '../api/connections';
|
||||
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
|
||||
import { getClashAPIConfig } from '../store/app';
|
||||
import Button from './Button';
|
||||
import s from './Connections.module.scss';
|
||||
import ConnectionTable from './ConnectionTable';
|
||||
import ContentHeader from './ContentHeader';
|
||||
|
@ -58,22 +59,38 @@ function hasSubstring(s: string, pat: string) {
|
|||
return s.toLowerCase().includes(pat.toLowerCase());
|
||||
}
|
||||
|
||||
function filterConns(conns: FormattedConn[], keyword: string) {
|
||||
return !keyword
|
||||
? conns
|
||||
: conns.filter((conn) =>
|
||||
[
|
||||
conn.host,
|
||||
conn.sourceIP,
|
||||
conn.sourcePort,
|
||||
conn.destinationIP,
|
||||
conn.chains,
|
||||
conn.rule,
|
||||
conn.type,
|
||||
conn.network,
|
||||
conn.process,
|
||||
].some((field) => hasSubstring(field, keyword))
|
||||
function filterConnIps(conns: FormattedConn[], ipStr: string) {
|
||||
return conns.filter((each) => each.sourceIP === ipStr);
|
||||
}
|
||||
|
||||
function filterConns(conns: FormattedConn[], keyword: string, sourceIp: string) {
|
||||
let result = conns;
|
||||
if (keyword !== '') {
|
||||
result = conns.filter((conn) =>
|
||||
[
|
||||
conn.host,
|
||||
conn.sourceIP,
|
||||
conn.sourcePort,
|
||||
conn.destinationIP,
|
||||
conn.chains,
|
||||
conn.rule,
|
||||
conn.type,
|
||||
conn.network,
|
||||
conn.process,
|
||||
].some((field) => {
|
||||
return hasSubstring(field, keyword);
|
||||
})
|
||||
);
|
||||
}
|
||||
if (sourceIp !== '') {
|
||||
result = filterConnIps(result, sourceIp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getConnIpList(conns: FormattedConn[]) {
|
||||
return Array.from(new Set(conns.map((x) => x.sourceIP))).sort();
|
||||
}
|
||||
|
||||
function formatConnectionDataItem(
|
||||
|
@ -103,7 +120,7 @@ function formatConnectionDataItem(
|
|||
upload,
|
||||
download,
|
||||
start: now - new Date(start).valueOf(),
|
||||
chains: chains.reverse().join(' / '),
|
||||
chains: modifyChains(chains),
|
||||
rule: !rulePayload ? rule : `${rule} :: ${rulePayload}`,
|
||||
...metadata,
|
||||
host: `${host2}:${destinationPort}`,
|
||||
|
@ -117,6 +134,24 @@ function formatConnectionDataItem(
|
|||
};
|
||||
return ret;
|
||||
}
|
||||
function modifyChains(chains: string[]): string {
|
||||
if (!Array.isArray(chains) || chains.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (chains.length === 1) {
|
||||
return chains[0];
|
||||
}
|
||||
|
||||
//倒序
|
||||
if (chains.length === 2) {
|
||||
return chains[1] + ' / ' + chains[0];
|
||||
}
|
||||
|
||||
const first = chains.pop();
|
||||
const last = chains.shift();
|
||||
return `${first} / ${last}`;
|
||||
}
|
||||
|
||||
function renderTableOrPlaceholder(conns: FormattedConn[]) {
|
||||
return conns.length > 0 ? (
|
||||
|
@ -134,11 +169,19 @@ function ConnQty({ qty }) {
|
|||
|
||||
function Conn({ apiConfig }) {
|
||||
const [refContainer, containerHeight] = useRemainingViewPortHeight();
|
||||
|
||||
const [conns, setConns] = useState([]);
|
||||
const [closedConns, setClosedConns] = useState([]);
|
||||
|
||||
const [filterKeyword, setFilterKeyword] = useState('');
|
||||
const filteredConns = filterConns(conns, filterKeyword);
|
||||
const filteredClosedConns = filterConns(closedConns, filterKeyword);
|
||||
const [filterSourceIpStr, setFilterSourceIpStr] = useState('');
|
||||
|
||||
const filteredConns = filterConns(conns, filterKeyword, filterSourceIpStr);
|
||||
const filteredClosedConns = filterConns(closedConns, filterKeyword, filterSourceIpStr);
|
||||
|
||||
const connIpSet = getConnIpList(conns);
|
||||
const ClosedConnIpSet = getConnIpList(closedConns);
|
||||
|
||||
const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false);
|
||||
const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
|
||||
const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []);
|
||||
|
@ -178,9 +221,15 @@ function Conn({ apiConfig }) {
|
|||
},
|
||||
[setConns, isRefreshPaused]
|
||||
);
|
||||
const [reConnectCount, setReConnectCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
return connAPI.fetchData(apiConfig, read);
|
||||
}, [apiConfig, read]);
|
||||
return connAPI.fetchData(apiConfig, read, () => {
|
||||
setTimeout(() => {
|
||||
setReConnectCount((prev) => prev + 1);
|
||||
}, 1000);
|
||||
});
|
||||
}, [apiConfig, read, reConnectCount, setReConnectCount]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -230,7 +279,17 @@ function Conn({ apiConfig }) {
|
|||
}}
|
||||
>
|
||||
<TabPanel>
|
||||
<>{renderTableOrPlaceholder(filteredConns)}</>
|
||||
<Button onClick={() => setFilterSourceIpStr('')} kind="minimal">
|
||||
{t('All')}
|
||||
</Button>
|
||||
{connIpSet.map((value, k) => {
|
||||
return (
|
||||
<Button key={k} onClick={() => setFilterSourceIpStr(value)} kind="minimal">
|
||||
{value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{renderTableOrPlaceholder(filteredConns)}
|
||||
<Fab
|
||||
icon={isRefreshPaused ? <Play size={16} /> : <Pause size={16} />}
|
||||
mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}}
|
||||
|
@ -243,7 +302,19 @@ function Conn({ apiConfig }) {
|
|||
</Action>
|
||||
</Fab>
|
||||
</TabPanel>
|
||||
<TabPanel>{renderTableOrPlaceholder(filteredClosedConns)}</TabPanel>
|
||||
<TabPanel>
|
||||
<Button onClick={() => setFilterSourceIpStr('')} kind="minimal">
|
||||
{t('All')}
|
||||
</Button>
|
||||
{ClosedConnIpSet.map((value, k) => {
|
||||
return (
|
||||
<Button key={k} onClick={() => setFilterSourceIpStr(value)} kind="minimal">
|
||||
{value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{renderTableOrPlaceholder(filteredClosedConns)}
|
||||
</TabPanel>
|
||||
</div>
|
||||
</div>
|
||||
<ModalCloseAllConnections
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
.root {
|
||||
padding: 6px 15px;
|
||||
@media (--breakpoint-not-small) {
|
||||
@media screen and (min-width: 30em) {
|
||||
padding: 10px 40px;
|
||||
}
|
||||
}
|
||||
.chart {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import ContentHeader from './ContentHeader';
|
||||
import s0 from './Home.module.scss';
|
||||
import Loading from './Loading';
|
||||
import MemoryChart from './MemoryChart';
|
||||
import TrafficChart from './TrafficChart';
|
||||
import TrafficNow from './TrafficNow';
|
||||
|
||||
|
@ -19,6 +20,7 @@ export default function Home() {
|
|||
<div className={s0.chart}>
|
||||
<Suspense fallback={<Loading height="200px" />}>
|
||||
<TrafficChart />
|
||||
<MemoryChart />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
|
61
src/components/MemoryChart.tsx
Normal file
61
src/components/MemoryChart.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { State } from '~/store/types';
|
||||
|
||||
import { fetchData } from '../api/memory';
|
||||
import { useLineChartMemory } from '../hooks/useLineChart';
|
||||
import {
|
||||
chartJSResource,
|
||||
chartStyles,
|
||||
commonDataSetProps,
|
||||
memoryChartOptions,
|
||||
} from '../misc/chart-memory';
|
||||
import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app';
|
||||
import { connect } from './StateProvider';
|
||||
|
||||
const { useMemo } = React;
|
||||
|
||||
const chartWrapperStyle = {
|
||||
// make chartjs chart responsive
|
||||
position: 'relative',
|
||||
maxWidth: 1000,
|
||||
marginTop: '1em',
|
||||
};
|
||||
|
||||
const mapState = (s: State) => ({
|
||||
apiConfig: getClashAPIConfig(s),
|
||||
selectedChartStyleIndex: getSelectedChartStyleIndex(s),
|
||||
});
|
||||
|
||||
export default connect(mapState)(MemoryChart);
|
||||
|
||||
function MemoryChart({ apiConfig, selectedChartStyleIndex }) {
|
||||
const ChartMod = chartJSResource.read();
|
||||
const memory = fetchData(apiConfig);
|
||||
const { t } = useTranslation();
|
||||
const data = useMemo(
|
||||
() => ({
|
||||
labels: memory.labels,
|
||||
datasets: [
|
||||
{
|
||||
...commonDataSetProps,
|
||||
...memoryChartOptions,
|
||||
...chartStyles[selectedChartStyleIndex].inuse,
|
||||
label: t('Memory'),
|
||||
data: memory.inuse,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[memory, selectedChartStyleIndex, t]
|
||||
);
|
||||
|
||||
useLineChartMemory(ChartMod.Chart, 'MemoryChart', data, memory);
|
||||
|
||||
return (
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message
|
||||
<div style={chartWrapperStyle}>
|
||||
<canvas id="MemoryChart" />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -11,11 +11,10 @@ import { connect } from './StateProvider';
|
|||
|
||||
const { useMemo } = React;
|
||||
|
||||
const chartWrapperStyle = {
|
||||
const chartWrapperStyle: React.CSSProperties = {
|
||||
// make chartjs chart responsive
|
||||
position: 'relative',
|
||||
maxWidth: 1000,
|
||||
marginTop: '1em',
|
||||
};
|
||||
|
||||
const mapState = (s: State) => ({
|
||||
|
@ -53,7 +52,6 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
|
|||
useLineChart(ChartMod.Chart, 'trafficChart', data, traffic);
|
||||
|
||||
return (
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message
|
||||
<div style={chartWrapperStyle}>
|
||||
<canvas id="trafficChart" />
|
||||
</div>
|
||||
|
|
|
@ -3,26 +3,25 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 140px);
|
||||
grid-gap: 20px;
|
||||
justify-content: space-around;
|
||||
max-width: 1000px;
|
||||
|
||||
.sec {
|
||||
padding: 10px;
|
||||
width: 19%;
|
||||
margin: 3px;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
div:nth-child(1) {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.65em;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
div:nth-child(2) {
|
||||
padding: 10px 0 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
width: 48%;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export default connect(mapState)(TrafficNow);
|
|||
function TrafficNow({ apiConfig }) {
|
||||
const { t } = useTranslation();
|
||||
const { upStr, downStr } = useSpeed(apiConfig);
|
||||
const { upTotal, dlTotal, connNumber } = useConnection(apiConfig);
|
||||
const { upTotal, dlTotal, connNumber, mUsage } = useConnection(apiConfig);
|
||||
return (
|
||||
<div className={s0.TrafficNow}>
|
||||
<div className={s0.sec}>
|
||||
|
@ -41,6 +41,10 @@ function TrafficNow({ apiConfig }) {
|
|||
<div>{t('Active Connections')}</div>
|
||||
<div>{connNumber}</div>
|
||||
</div>
|
||||
<div className={s0.sec}>
|
||||
<div>{t('Memory Usage')}</div>
|
||||
<div>{mUsage}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -63,13 +67,15 @@ function useConnection(apiConfig) {
|
|||
upTotal: '0 B',
|
||||
dlTotal: '0 B',
|
||||
connNumber: 0,
|
||||
mUsage: '0 B',
|
||||
});
|
||||
const read = useCallback(
|
||||
({ downloadTotal, uploadTotal, connections }) => {
|
||||
({ downloadTotal, uploadTotal, connections, memory }) => {
|
||||
setState({
|
||||
upTotal: prettyBytes(uploadTotal),
|
||||
dlTotal: prettyBytes(downloadTotal),
|
||||
connNumber: connections.length,
|
||||
mUsage: prettyBytes(memory),
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
|
|
|
@ -76,8 +76,8 @@
|
|||
|
||||
.proxySmall {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
|
||||
.now {
|
||||
|
|
|
@ -50,7 +50,7 @@ function ProxyGroupImpl({
|
|||
);
|
||||
|
||||
const isSelectable = useMemo(
|
||||
() => ['Selector', version.meta && 'Fallback'].includes(type),
|
||||
() => ['Selector', version.meta && 'Fallback', version.meta && 'URLTest'].includes(type),
|
||||
[type, version.meta]
|
||||
);
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
.proxyLatency {
|
||||
border-radius: 20px;
|
||||
color: #eee;
|
||||
font-size: 0.6em;
|
||||
font-size: 1em;
|
||||
@media (--breakpoint-not-small) {
|
||||
font-size: 0.7em;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ function ProxyProviderImpl({
|
|||
const getYear = expire.getFullYear() + '-';
|
||||
const getMonth =
|
||||
(expire.getMonth() + 1 < 10 ? '0' + (expire.getMonth() + 1) : expire.getMonth() + 1) + '-';
|
||||
const getDate = expire.getDate() + ' ';
|
||||
const getDate = (expire.getDate() < 10 ? '0' + expire.getDate() : expire.getDate()) + ' ';
|
||||
return getYear + getMonth + getDate;
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -4,7 +4,7 @@ h2.sectionNameType {
|
|||
margin: 0;
|
||||
font-size: 1em;
|
||||
@media (--breakpoint-not-small) {
|
||||
font-size: 1.1em;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { ChartConfiguration } from 'chart.js';
|
|||
import React from 'react';
|
||||
|
||||
import { commonChartOptions } from '~/misc/chart';
|
||||
import { memoryChartOptions } from '~/misc/chart-memory';
|
||||
|
||||
const { useEffect } = React;
|
||||
|
||||
|
@ -23,3 +24,22 @@ export default function useLineChart(
|
|||
};
|
||||
}, [chart, elementId, data, subscription, extraChartOptions]);
|
||||
}
|
||||
|
||||
export function useLineChartMemory(
|
||||
chart: typeof import('chart.js').Chart,
|
||||
elementId: string,
|
||||
data: ChartConfiguration['data'],
|
||||
subscription: any,
|
||||
extraChartOptions = {}
|
||||
) {
|
||||
useEffect(() => {
|
||||
const ctx = (document.getElementById(elementId) as HTMLCanvasElement).getContext('2d');
|
||||
const options = { ...memoryChartOptions, ...extraChartOptions };
|
||||
const c = new chart(ctx, { type: 'line', data, options });
|
||||
const unsubscribe = subscription && subscription.subscribe(() => c.update());
|
||||
return () => {
|
||||
unsubscribe && unsubscribe();
|
||||
c.destroy();
|
||||
};
|
||||
}, [chart, elementId, data, subscription, extraChartOptions]);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const data = {
|
||||
All: 'All',
|
||||
Overview: 'Overview',
|
||||
Proxies: 'Proxies',
|
||||
Rules: 'Rules',
|
||||
|
@ -10,6 +11,7 @@ export const data = {
|
|||
'Upload Total': 'Upload Total',
|
||||
'Download Total': 'Download Total',
|
||||
'Active Connections': 'Active Connections',
|
||||
'Memory Usage': 'Memory Usage',
|
||||
'Pause Refresh': 'Pause Refresh',
|
||||
'Resume Refresh': 'Resume Refresh',
|
||||
close_all_connections: 'Close All Connections',
|
||||
|
@ -39,6 +41,8 @@ export const data = {
|
|||
update_all_rule_provider: 'Update all rule providers',
|
||||
update_all_proxy_provider: 'Update all proxy providers',
|
||||
reload_config_file: 'Reload config file',
|
||||
restart_core: 'Restart clash core',
|
||||
upgrade_core: 'Upgrade Alpha core',
|
||||
update_geo_databases_file: 'Update GEO Databases ',
|
||||
flush_fake_ip_pool: 'Flush fake-ip data',
|
||||
enable_tun_device: 'Enable TUN Device',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const data = {
|
||||
All: '所有',
|
||||
Overview: '概览',
|
||||
Proxies: '代理',
|
||||
Rules: '规则',
|
||||
|
@ -10,6 +11,8 @@ export const data = {
|
|||
'Upload Total': '上传总量',
|
||||
'Download Total': '下载总量',
|
||||
'Active Connections': '活动连接',
|
||||
'Memory Usage': '内存使用情况',
|
||||
Memory: '内存',
|
||||
'Pause Refresh': '暂停刷新',
|
||||
'Resume Refresh': '继续刷新',
|
||||
close_all_connections: '关闭所有连接',
|
||||
|
@ -57,4 +60,6 @@ export const data = {
|
|||
c_source: '来源',
|
||||
c_destination_ip: '目标IP',
|
||||
c_type: '类型',
|
||||
restart_core: '重启 clash 核心',
|
||||
upgrade_core: '更新 Alpha 核心',
|
||||
};
|
||||
|
|
64
src/misc/chart-memory.ts
Normal file
64
src/misc/chart-memory.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { createAsset } from 'use-asset';
|
||||
|
||||
import prettyBytes from './pretty-bytes';
|
||||
export const chartJSResource = createAsset(() => {
|
||||
return import('~/misc/chart-lib');
|
||||
});
|
||||
|
||||
export const commonDataSetProps = { borderWidth: 1, pointRadius: 0, tension: 0.2, fill: true };
|
||||
|
||||
export const memoryChartOptions: import('chart.js').ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: { labels: { boxWidth: 20 } },
|
||||
},
|
||||
scales: {
|
||||
x: { display: false, type: 'category' },
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
grid: {
|
||||
display: true,
|
||||
color: '#555',
|
||||
drawTicks: false,
|
||||
},
|
||||
border: {
|
||||
dash: [3, 6],
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
callback(value: number) {
|
||||
return prettyBytes(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const chartStyles = [
|
||||
{
|
||||
inuse: {
|
||||
backgroundColor: 'rgba(81, 168, 221, 0.5)',
|
||||
borderColor: 'rgb(81, 168, 221)',
|
||||
},
|
||||
},
|
||||
{
|
||||
inuse: {
|
||||
backgroundColor: 'rgba(245,78,162,0.6)',
|
||||
borderColor: 'rgba(245,78,162,1)',
|
||||
},
|
||||
},
|
||||
{
|
||||
inuse: {
|
||||
backgroundColor: 'rgba(94, 175, 223, 0.3)',
|
||||
borderColor: 'rgb(94, 175, 223)',
|
||||
},
|
||||
},
|
||||
{
|
||||
inuse: {
|
||||
backgroundColor: 'rgba(242, 174, 62, 0.3)',
|
||||
borderColor: 'rgb(242, 174, 62)',
|
||||
},
|
||||
},
|
||||
];
|
|
@ -27,6 +27,7 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = {
|
|||
dash: [3, 6],
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 5,
|
||||
callback(value: number) {
|
||||
return prettyBytes(value) + '/s ';
|
||||
},
|
||||
|
@ -38,22 +39,22 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = {
|
|||
export const chartStyles = [
|
||||
{
|
||||
down: {
|
||||
backgroundColor: 'rgba(176, 209, 132, 0.8)',
|
||||
borderColor: 'rgb(176, 209, 132)',
|
||||
backgroundColor: 'rgba(81, 168, 221, 0.5)',
|
||||
borderColor: 'rgb(81, 168, 221)',
|
||||
},
|
||||
up: {
|
||||
backgroundColor: 'rgba(181, 220, 231, 0.8)',
|
||||
borderColor: 'rgb(181, 220, 231)',
|
||||
backgroundColor: 'rgba(219, 77, 109, 0.5)',
|
||||
borderColor: 'rgb(219, 77, 109)',
|
||||
},
|
||||
},
|
||||
{
|
||||
up: {
|
||||
backgroundColor: 'rgb(98, 190, 100)',
|
||||
borderColor: 'rgb(78,146,79)',
|
||||
backgroundColor: 'rgba(245,78,162,0.6)',
|
||||
borderColor: 'rgba(245,78,162,1)',
|
||||
},
|
||||
down: {
|
||||
backgroundColor: 'rgb(160, 230, 66)',
|
||||
borderColor: 'rgb(110, 156, 44)',
|
||||
backgroundColor: 'rgba(123,59,140,0.6)',
|
||||
borderColor: 'rgba(66,33,142,1)',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -47,7 +47,7 @@ i18next
|
|||
}
|
||||
},
|
||||
},
|
||||
supportedLngs: ['en', 'zh'],
|
||||
supportedLngs: ['zh', 'en'],
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
|
|
@ -95,7 +95,7 @@ export function updateClashAPIConfig({ baseURL, secret }) {
|
|||
const rootEl = document.querySelector('html');
|
||||
type ThemeType = 'dark' | 'light' | 'auto';
|
||||
|
||||
function setTheme(theme: ThemeType = 'dark') {
|
||||
function setTheme(theme: ThemeType = 'light') {
|
||||
if (theme === 'auto') {
|
||||
rootEl.setAttribute('data-theme', 'auto');
|
||||
} else if (theme === 'dark') {
|
||||
|
@ -159,7 +159,7 @@ const defaultState: StateApp = {
|
|||
selectedClashAPIConfigIndex: 0,
|
||||
clashAPIConfigs: [defaultClashAPIConfig],
|
||||
|
||||
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
||||
latencyTestUrl: 'https://www.gstatic.com/generate_204',
|
||||
selectedChartStyleIndex: 0,
|
||||
theme: 'dark',
|
||||
|
||||
|
|
|
@ -115,6 +115,51 @@ export function reloadConfigFile(apiConfig: ClashAPIConfig) {
|
|||
};
|
||||
}
|
||||
|
||||
export function restartCore(apiConfig: ClashAPIConfig) {
|
||||
return async (dispatch: DispatchFn) => {
|
||||
configsAPI
|
||||
.restartCore(apiConfig)
|
||||
.then(
|
||||
(res) => {
|
||||
if (res.ok === false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error restart core', res.statusText);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error restart core', err);
|
||||
throw err;
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(fetchConfigs(apiConfig));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function upgradeCore(apiConfig: ClashAPIConfig) {
|
||||
return async (dispatch: DispatchFn) => {
|
||||
configsAPI
|
||||
.upgradeCore(apiConfig)
|
||||
.then(
|
||||
(res) => {
|
||||
if (res.ok === false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error upgrade core', res.statusText);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error upgrade core', err);
|
||||
throw err;
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(fetchConfigs(apiConfig));
|
||||
});
|
||||
};
|
||||
}
|
||||
export function updateGeoDatabasesFile(apiConfig: ClashAPIConfig) {
|
||||
return async (dispatch: DispatchFn) => {
|
||||
configsAPI
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
@import '@fontsource/roboto-mono/latin-400.css';
|
||||
@import 'modern-normalize/modern-normalize.css';
|
||||
|
||||
@font-face {
|
||||
font-family: '_Twemoji Mozilla';
|
||||
src: url('../../assets/Twemoji_Mozilla.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -59,7 +64,7 @@
|
|||
:root {
|
||||
--font-mono: 'Roboto Mono', Menlo, monospace;
|
||||
// prettier-ignore
|
||||
--font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, "Twemoji Mozilla", Segoe UI Emoji, Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '\5fae\8f6f\96c5\9ed1', Arial;
|
||||
--font-normal: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Source Han Sans", "PingFang SC", "Microsoft YaHei" , "微软雅黑", Arial,"Twemoji Mozilla", "_Twemoji Mozilla","Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--color-focus-blue: #1a73e8;
|
||||
--btn-bg: #387cec;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue