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

View file

@ -15,11 +15,16 @@
border-radius: 10px;
display: grid;
place-content: center;
grid-template-columns: 40px 1fr 40px;
grid-template-rows: 30px;
grid-template-areas: 'close url .';
grid-template-columns: 40px 1fr;
column-gap: 10px;
border: 1px solid var(--bg-near-transparent);
.right {
display: grid;
column-gap: 10px;
grid-template-columns: 1fr 40px;
grid-auto-rows: 30px;
}
}
.li:hover {
@ -28,7 +33,6 @@
.close {
opacity: 0;
grid-area: close;
place-self: center;
cursor: pointer;
}
@ -42,28 +46,15 @@
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 {
grid-area: eye;
opacity: 0;
place-self: center;
cursor: pointer;
}
.url,
.secret {
.secret,
.metaLabel {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -94,9 +85,10 @@
color: var(--color-text-secondary);
}
.url {
.url,
.metaLabel {
cursor: pointer;
}
.url:hover {
color: var(--color-text-highlight);
&:hover {
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 { ClashAPIConfig } from 'src/types';
import { State } from '$src/store/types';
import s from './BackendList.module.scss';
import { connect, useStoreActions } from './StateProvider';
type Config = ClashAPIConfig & { addedAt: number };
const mapState = (s) => ({
const mapState = (s: State) => ({
apiConfigs: getClashAPIConfigs(s),
selectedClashAPIConfigIndex: getSelectedClashAPIConfigIndex(s),
});
@ -47,16 +49,12 @@ function BackendListImpl({
{apiConfigs.map((item, idx) => {
return (
<li
className={cx(s.li, {
[s.hasSecret]: item.secret,
[s.isSelected]: idx === selectedClashAPIConfigIndex,
})}
key={item.baseURL + item.secret}
className={cx(s.li, { [s.isSelected]: idx === selectedClashAPIConfigIndex })}
key={item.baseURL + item.secret + item.metaLabel}
>
<Item
disableRemove={idx === selectedClashAPIConfigIndex}
baseURL={item.baseURL}
secret={item.secret}
conf={item}
onRemove={onRemove}
onSelect={onSelect}
/>
@ -69,14 +67,12 @@ function BackendListImpl({
}
function Item({
baseURL,
secret,
conf,
disableRemove,
onRemove,
onSelect,
}: {
baseURL: string;
secret: string;
conf: ClashAPIConfig;
disableRemove: boolean;
onRemove: (x: ClashAPIConfig) => void;
onSelect: (x: ClashAPIConfig) => void;
@ -90,31 +86,44 @@ function Item({
return (
<>
<Button
disabled={disableRemove}
onClick={() => onRemove({ baseURL, secret })}
className={s.close}
>
<Button disabled={disableRemove} onClick={() => onRemove(conf)} className={s.close}>
<Close size={20} />
</Button>
<span
className={s.url}
tabIndex={0}
role="button"
onClick={() => onSelect({ baseURL, secret })}
onKeyUp={handleTap}
>
{baseURL}
</span>
<span />
{secret ? (
<>
<span className={s.secret}>{show ? secret : '***'}</span>
<Button onClick={toggle} className={s.eye}>
<Icon size={20} />
</Button>
</>
) : null}
<div className={s.right}>
{conf.metaLabel ? (
<>
<span
className={s.metaLabel}
tabIndex={0}
role="button"
onClick={() => onSelect(conf)}
onKeyUp={handleTap}
>
{conf.metaLabel}
</span>
<span />
</>
) : null}
<span
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 { 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),
apiConfigs: getClashAPIConfigs(s),
});
function HeadImpl({
apiConfig,
apiConfigs,
}: {
apiConfig: { baseURL: string };
apiConfigs: any[];
}) {
function HeadImpl({ apiConfig, apiConfigs }: { apiConfig: ClashAPIConfig; apiConfigs: any[] }) {
React.useEffect(() => {
let title = 'yacd';
if (apiConfigs.length > 1) {
try {
const host = new URL(apiConfig.baseURL).host;
title = `${host} - yacd`;
title = `${apiConfig.metaLabel || new URL(apiConfig.baseURL).host} - yacd`;
} catch (e) {
// ignore
}

View file

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

View file

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