3x-ui/frontend/src/hooks/useAllSetting.ts
MHSanaei d50ec74b24
refactor(frontend): port settings to react+ts
Step 5 of the planned vue->react migration. Settings is the first
entry whose state model didn't translate to the Vue-style "parent
passes a reactive object, children mutate it in place" pattern, so
the React port flips it to lifted state + a typed updateSetting
patch function.

* models/setting.ts — typed AllSetting class with the same field
  defaults and equals() behavior the vue version had. The .js
  twin is deleted; nothing else imported it.
* hooks/useAllSetting.ts — owns allSetting + oldAllSetting state,
  exposes updateSetting(patch), saveDisabled is derived via useMemo
  off equals() (no more 1Hz dirty-check timer).
* components/SettingListItem.tsx — children-based wrapper instead
  of named slots. The vue twin stays alive because xray (BasicsTab,
  DnsTab) still imports it; deleted when xray migrates.

The five tab components and the TwoFactorModal each accept
{ allSetting, updateSetting } and render with AntD v5's Collapse
items[] API. Every v-model:value="x" became
value={...} onChange={(e) => updateSetting({ key: e.target.value })}
or onChange={(v) => updateSetting({ key: v })} for non-input
controls.

SubscriptionFormatsTab is the trickiest — fragment / noises[] /
mux / direct routing rules are stored as JSON-encoded strings on
the wire. Parsing them once via useMemo per field, mutating the
parsed object on edit, and stringifying back into the patch keeps
the round-trip identical to the vue version.

SettingsPage hosts the tab navigation (with hash sync), the
save / restart action bar, the security-warnings alert banner,
and the restart flow that rebuilds the panel URL after the new
host/port/cert settings take effect.
2026-05-21 21:48:15 +02:00

69 lines
1.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HttpUtil } from '@/utils';
import { AllSetting } from '@/models/setting';
interface ApiMsg<T = unknown> {
success?: boolean;
obj?: T;
}
export function useAllSetting() {
const [allSetting, setAllSetting] = useState<AllSetting>(() => new AllSetting());
const [oldAllSetting, setOldAllSetting] = useState<AllSetting>(() => new AllSetting());
const [fetched, setFetched] = useState(false);
const [spinning, setSpinning] = useState(false);
const fetchedRef = useRef(false);
const applyServerState = useCallback((obj: unknown) => {
setAllSetting(new AllSetting(obj));
setOldAllSetting(new AllSetting(obj));
}, []);
const fetchAll = useCallback(async () => {
const msg = await HttpUtil.post('/panel/setting/all') as ApiMsg;
if (msg?.success) {
applyServerState(msg.obj);
fetchedRef.current = true;
setFetched(true);
}
}, [applyServerState]);
const saveAll = useCallback(async () => {
setSpinning(true);
try {
const msg = await HttpUtil.post('/panel/setting/update', allSetting) as ApiMsg;
if (msg?.success) await fetchAll();
} finally {
setSpinning(false);
}
}, [allSetting, fetchAll]);
const updateSetting = useCallback((patch: Partial<AllSetting>) => {
setAllSetting((prev) => {
const next = new AllSetting(prev);
Object.assign(next, patch);
return next;
});
}, []);
const saveDisabled = useMemo(
() => allSetting.equals(oldAllSetting),
[allSetting, oldAllSetting],
);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchAll();
}, [fetchAll]);
return {
allSetting,
updateSetting,
fetched,
spinning,
setSpinning,
saveDisabled,
fetchAll,
saveAll,
};
}