Support label API backend

for #713
This commit is contained in:
Haishan 2022-09-10 14:21:58 +08:00
parent 58d3db090d
commit 46ca343aed
6 changed files with 107 additions and 84 deletions

View file

@ -14,6 +14,9 @@ import SvgYacd from './SvgYacd';
const { useState, useRef, useCallback, useEffect } = React; const { useState, useRef, useCallback, useEffect } = React;
const Ok = 0; const Ok = 0;
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const mapState = (s: State) => ({ const mapState = (s: State) => ({
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),
}); });
@ -21,12 +24,13 @@ const mapState = (s: State) => ({
function APIConfig({ dispatch }) { function APIConfig({ dispatch }) {
const [baseURL, setBaseURL] = useState(''); const [baseURL, setBaseURL] = useState('');
const [secret, setSecret] = useState(''); const [secret, setSecret] = useState('');
const [metaLabel, setMetaLabel] = useState('');
const [errMsg, setErrMsg] = useState(''); const [errMsg, setErrMsg] = useState('');
const userTouchedFlagRef = useRef(false); const userTouchedFlagRef = useRef(false);
const contentEl = useRef(null); const contentEl = useRef(null);
const handleInputOnChange = useCallback((e) => { const handleInputOnChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((e) => {
userTouchedFlagRef.current = true; userTouchedFlagRef.current = true;
setErrMsg(''); setErrMsg('');
const target = e.target; const target = e.target;
@ -39,6 +43,9 @@ function APIConfig({ dispatch }) {
case 'secret': case 'secret':
setSecret(value); setSecret(value);
break; break;
case 'metaLabel':
setMetaLabel(value);
break;
default: default:
throw new Error(`unknown input name ${name}`); throw new Error(`unknown input name ${name}`);
} }
@ -49,10 +56,10 @@ function APIConfig({ dispatch }) {
if (ret[0] !== Ok) { if (ret[0] !== Ok) {
setErrMsg(ret[1]); setErrMsg(ret[1]);
} else { } else {
dispatch(addClashAPIConfig({ baseURL, secret })); dispatch(addClashAPIConfig({ baseURL, secret, metaLabel }));
} }
}); });
}, [baseURL, secret, dispatch]); }, [baseURL, secret, metaLabel, dispatch]);
const handleContentOnKeyDown = useCallback( const handleContentOnKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
@ -76,7 +83,7 @@ function APIConfig({ dispatch }) {
if (data['hello'] === 'clash') { if (data['hello'] === 'clash') {
setBaseURL(window.location.origin); setBaseURL(window.location.origin);
} }
}); }, noop);
}; };
useEffect(() => { useEffect(() => {
detectApiServer(); detectApiServer();
@ -110,8 +117,19 @@ function APIConfig({ dispatch }) {
onChange={handleInputOnChange} onChange={handleInputOnChange}
/> />
</div> </div>
{errMsg ? <div className={s0.error}>{errMsg}</div> : null}
<div className={s0.label}>
<Field
id="metaLabel"
name="metaLabel"
label="Label(optional)"
type="text"
placeholder=""
value={metaLabel}
onChange={handleInputOnChange}
/>
</div>
</div> </div>
<div className={s0.error}>{errMsg ? errMsg : null}</div>
<div className={s0.footer}> <div className={s0.footer}>
<Button label="Add" onClick={onConfirm} /> <Button label="Add" onClick={onConfirm} />
</div> </div>

View file

@ -15,11 +15,16 @@
border-radius: 10px; border-radius: 10px;
display: grid; display: grid;
place-content: center; place-content: center;
grid-template-columns: 40px 1fr 40px; grid-template-columns: 40px 1fr;
grid-template-rows: 30px;
grid-template-areas: 'close url .';
column-gap: 10px; column-gap: 10px;
border: 1px solid var(--bg-near-transparent); border: 1px solid var(--bg-near-transparent);
.right {
display: grid;
column-gap: 10px;
grid-template-columns: 1fr 40px;
grid-auto-rows: 30px;
}
} }
.li:hover { .li:hover {
@ -28,7 +33,6 @@
.close { .close {
opacity: 0; opacity: 0;
grid-area: close;
place-self: center; place-self: center;
cursor: pointer; cursor: pointer;
} }
@ -42,28 +46,15 @@
opacity: 1; opacity: 1;
} }
.hasSecret {
grid-template-rows: repeat(2, 30px);
grid-template-areas:
'close url .'
'close secret eye';
}
.url {
grid-area: url;
}
.secret {
grid-area: secret;
}
.eye { .eye {
grid-area: eye;
opacity: 0; opacity: 0;
place-self: center; place-self: center;
cursor: pointer; cursor: pointer;
} }
.url, .url,
.secret { .secret,
.metaLabel {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -94,9 +85,10 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.url { .url,
.metaLabel {
cursor: pointer; cursor: pointer;
} &:hover {
.url:hover { color: var(--color-text-highlight);
color: var(--color-text-highlight); }
} }

View file

@ -5,12 +5,14 @@ import { useToggle } from 'src/hooks/basic';
import { getClashAPIConfigs, getSelectedClashAPIConfigIndex } from 'src/store/app'; import { getClashAPIConfigs, getSelectedClashAPIConfigIndex } from 'src/store/app';
import { ClashAPIConfig } from 'src/types'; import { ClashAPIConfig } from 'src/types';
import { State } from '$src/store/types';
import s from './BackendList.module.scss'; import s from './BackendList.module.scss';
import { connect, useStoreActions } from './StateProvider'; import { connect, useStoreActions } from './StateProvider';
type Config = ClashAPIConfig & { addedAt: number }; type Config = ClashAPIConfig & { addedAt: number };
const mapState = (s) => ({ const mapState = (s: State) => ({
apiConfigs: getClashAPIConfigs(s), apiConfigs: getClashAPIConfigs(s),
selectedClashAPIConfigIndex: getSelectedClashAPIConfigIndex(s), selectedClashAPIConfigIndex: getSelectedClashAPIConfigIndex(s),
}); });
@ -47,16 +49,12 @@ function BackendListImpl({
{apiConfigs.map((item, idx) => { {apiConfigs.map((item, idx) => {
return ( return (
<li <li
className={cx(s.li, { className={cx(s.li, { [s.isSelected]: idx === selectedClashAPIConfigIndex })}
[s.hasSecret]: item.secret, key={item.baseURL + item.secret + item.metaLabel}
[s.isSelected]: idx === selectedClashAPIConfigIndex,
})}
key={item.baseURL + item.secret}
> >
<Item <Item
disableRemove={idx === selectedClashAPIConfigIndex} disableRemove={idx === selectedClashAPIConfigIndex}
baseURL={item.baseURL} conf={item}
secret={item.secret}
onRemove={onRemove} onRemove={onRemove}
onSelect={onSelect} onSelect={onSelect}
/> />
@ -69,14 +67,12 @@ function BackendListImpl({
} }
function Item({ function Item({
baseURL, conf,
secret,
disableRemove, disableRemove,
onRemove, onRemove,
onSelect, onSelect,
}: { }: {
baseURL: string; conf: ClashAPIConfig;
secret: string;
disableRemove: boolean; disableRemove: boolean;
onRemove: (x: ClashAPIConfig) => void; onRemove: (x: ClashAPIConfig) => void;
onSelect: (x: ClashAPIConfig) => void; onSelect: (x: ClashAPIConfig) => void;
@ -90,31 +86,44 @@ function Item({
return ( return (
<> <>
<Button <Button disabled={disableRemove} onClick={() => onRemove(conf)} className={s.close}>
disabled={disableRemove}
onClick={() => onRemove({ baseURL, secret })}
className={s.close}
>
<Close size={20} /> <Close size={20} />
</Button> </Button>
<span
className={s.url} <div className={s.right}>
tabIndex={0} {conf.metaLabel ? (
role="button" <>
onClick={() => onSelect({ baseURL, secret })} <span
onKeyUp={handleTap} className={s.metaLabel}
> tabIndex={0}
{baseURL} role="button"
</span> onClick={() => onSelect(conf)}
<span /> onKeyUp={handleTap}
{secret ? ( >
<> {conf.metaLabel}
<span className={s.secret}>{show ? secret : '***'}</span> </span>
<Button onClick={toggle} className={s.eye}> <span />
<Icon size={20} /> </>
</Button> ) : null}
</> <span
) : null} className={s.url}
tabIndex={0}
role="button"
onClick={() => onSelect(conf)}
onKeyUp={handleTap}
>
{conf.baseURL}
</span>
<span />
{conf.secret ? (
<>
<span className={s.secret}>{show ? conf.secret : '***'}</span>
<Button onClick={toggle} className={s.eye}>
<Icon size={16} />
</Button>
</>
) : null}
</div>
</> </>
); );
} }

View file

@ -2,24 +2,20 @@ import * as React from 'react';
import { connect } from 'src/components/StateProvider'; import { connect } from 'src/components/StateProvider';
import { getClashAPIConfig, getClashAPIConfigs } from 'src/store/app'; import { getClashAPIConfig, getClashAPIConfigs } from 'src/store/app';
const mapState = (s) => ({ import { State } from '$src/store/types';
import { ClashAPIConfig } from '$src/types';
const mapState = (s: State) => ({
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),
apiConfigs: getClashAPIConfigs(s), apiConfigs: getClashAPIConfigs(s),
}); });
function HeadImpl({ function HeadImpl({ apiConfig, apiConfigs }: { apiConfig: ClashAPIConfig; apiConfigs: any[] }) {
apiConfig,
apiConfigs,
}: {
apiConfig: { baseURL: string };
apiConfigs: any[];
}) {
React.useEffect(() => { React.useEffect(() => {
let title = 'yacd'; let title = 'yacd';
if (apiConfigs.length > 1) { if (apiConfigs.length > 1) {
try { try {
const host = new URL(apiConfig.baseURL).host; title = `${apiConfig.metaLabel || new URL(apiConfig.baseURL).host} - yacd`;
title = `${host} - yacd`;
} catch (e) { } catch (e) {
// ignore // ignore
} }

View file

@ -1,5 +1,7 @@
import { DispatchFn, GetStateFn, State, StateApp } from 'src/store/types'; import { DispatchFn, GetStateFn, State, StateApp } from 'src/store/types';
import { ClashAPIConfig } from '$src/types';
import { loadState, saveState } from '../misc/storage'; import { loadState, saveState } from '../misc/storage';
import { debounce, trimTrailingSlash } from '../misc/utils'; import { debounce, trimTrailingSlash } from '../misc/utils';
import { fetchConfigs } from './configs'; import { fetchConfigs } from './configs';
@ -22,21 +24,24 @@ export const getLogStreamingPaused = (s: State) => s.app.logStreamingPaused;
const saveStateDebounced = debounce(saveState, 600); const saveStateDebounced = debounce(saveState, 600);
function findClashAPIConfigIndex(getState: GetStateFn, { baseURL, secret }) { function findClashAPIConfigIndex(
getState: GetStateFn,
{ baseURL, secret, metaLabel }: ClashAPIConfig
) {
const arr = getClashAPIConfigs(getState()); const arr = getClashAPIConfigs(getState());
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
const x = arr[i]; const x = arr[i];
if (x.baseURL === baseURL && x.secret === secret) return i; if (x.baseURL === baseURL && x.secret === secret && x.metaLabel === metaLabel) return i;
} }
} }
export function addClashAPIConfig({ baseURL, secret }) { export function addClashAPIConfig(conf: ClashAPIConfig) {
return async (dispatch: DispatchFn, getState: GetStateFn) => { return async (dispatch: DispatchFn, getState: GetStateFn) => {
const idx = findClashAPIConfigIndex(getState, { baseURL, secret }); const idx = findClashAPIConfigIndex(getState, conf);
// already exists // already exists
if (idx) return; if (idx) return;
const clashAPIConfig = { baseURL, secret, addedAt: Date.now() }; const clashAPIConfig = { ...conf, addedAt: Date.now() };
dispatch('addClashAPIConfig', (s) => { dispatch('addClashAPIConfig', (s) => {
s.app.clashAPIConfigs.push(clashAPIConfig); s.app.clashAPIConfigs.push(clashAPIConfig);
}); });
@ -45,9 +50,9 @@ export function addClashAPIConfig({ baseURL, secret }) {
}; };
} }
export function removeClashAPIConfig({ baseURL, secret }) { export function removeClashAPIConfig(conf: ClashAPIConfig) {
return async (dispatch: DispatchFn, getState: GetStateFn) => { return async (dispatch: DispatchFn, getState: GetStateFn) => {
const idx = findClashAPIConfigIndex(getState, { baseURL, secret }); const idx = findClashAPIConfigIndex(getState, conf);
dispatch('removeClashAPIConfig', (s) => { dispatch('removeClashAPIConfig', (s) => {
s.app.clashAPIConfigs.splice(idx, 1); s.app.clashAPIConfigs.splice(idx, 1);
}); });
@ -56,9 +61,9 @@ export function removeClashAPIConfig({ baseURL, secret }) {
}; };
} }
export function selectClashAPIConfig({ baseURL, secret }) { export function selectClashAPIConfig(conf: ClashAPIConfig) {
return async (dispatch: DispatchFn, getState: GetStateFn) => { return async (dispatch: DispatchFn, getState: GetStateFn) => {
const idx = findClashAPIConfigIndex(getState, { baseURL, secret }); const idx = findClashAPIConfigIndex(getState, conf);
const curr = getSelectedClashAPIConfigIndex(getState()); const curr = getSelectedClashAPIConfigIndex(getState());
if (curr !== idx) { if (curr !== idx) {
dispatch('selectClashAPIConfig', (s) => { dispatch('selectClashAPIConfig', (s) => {
@ -79,9 +84,9 @@ export function selectClashAPIConfig({ baseURL, secret }) {
} }
// unused // unused
export function updateClashAPIConfig({ baseURL, secret }) { export function updateClashAPIConfig(conf: ClashAPIConfig) {
return async (dispatch: DispatchFn, getState: GetStateFn) => { return async (dispatch: DispatchFn, getState: GetStateFn) => {
const clashAPIConfig = { baseURL, secret }; const clashAPIConfig = conf;
dispatch('appUpdateClashAPIConfig', (s) => { dispatch('appUpdateClashAPIConfig', (s) => {
s.app.clashAPIConfigs[0] = clashAPIConfig; s.app.clashAPIConfigs[0] = clashAPIConfig;
}); });

View file

@ -1,6 +1,9 @@
export type ClashAPIConfig = { export type ClashAPIConfig = {
baseURL: string; baseURL: string;
secret?: string; secret?: string;
// metadata
metaLabel?: string;
}; };
export type LogsAPIConfig = ClashAPIConfig & { logLevel: string }; export type LogsAPIConfig = ClashAPIConfig & { logLevel: string };