This commit is contained in:
root
2026-05-17 07:16:36 +00:00
parent 71ea4da318
commit a10ffe332d
17 changed files with 178 additions and 99 deletions

View File

@@ -362,28 +362,62 @@ export function LiveYoutubeUnmannedView({
const [laneDrafts, setLaneDrafts] = useState<Record<string, ProLaneDraft>>({});
const [laneSaving, setLaneSaving] = useState(false);
const [laneStatus, setLaneStatus] = useState<string | null>(null);
const [activeViewRefreshToken, setActiveViewRefreshToken] = useState(0);
const urlListDirtyRef = useRef(false);
const singleUrlDraftRef = useRef(urlConfig);
const proUrlDraftRef = useRef(proUrlDraft);
const currentProcRef = useRef(currentProc);
const urlRequestSeqRef = useRef(0);
const ytIniRequestSeqRef = useRef(0);
const applySingleUrlDraft = useCallback(
(next: string) => {
singleUrlDraftRef.current = next;
setUrlConfig(next);
},
[setUrlConfig],
);
const applyProUrlDraft = useCallback((next: string) => {
proUrlDraftRef.current = next;
setProUrlDraft(next);
}, []);
useEffect(() => {
urlListDirtyRef.current = false;
}, [currentProc]);
useEffect(() => {
currentProcRef.current = currentProc;
}, [currentProc]);
useEffect(() => {
singleUrlDraftRef.current = urlConfig;
}, [urlConfig]);
useEffect(() => {
proUrlDraftRef.current = proUrlDraft;
}, [proUrlDraft]);
const pullUrlConfigFromServer = useCallback(async () => {
if (!currentProc) return;
const targetProc = currentProcRef.current.trim();
if (!targetProc) return;
const requestId = ++urlRequestSeqRef.current;
try {
const data = await fetchJson<{ content?: string }>(
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
`/get_url_config?process=${encodeURIComponent(targetProc)}`,
);
if (requestId !== urlRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
const c = data.content ?? "";
if (urlListDirtyRef.current) return;
if (mode === "single") setUrlConfig(c);
if (mode === "single") applySingleUrlDraft(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
applyProUrlDraft(localDraft);
return;
}
setProUrlDraft(c);
applyProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
@@ -395,7 +429,7 @@ export function LiveYoutubeUnmannedView({
} catch {
/* ignore */
}
}, [activeCh, channels, currentProc, fetchJson, mode, setUrlConfig]);
}, [activeCh, applyProUrlDraft, applySingleUrlDraft, channels, fetchJson, mode]);
useEffect(() => {
if (!currentProc || !urlAutoRefresh) return;
@@ -407,28 +441,31 @@ export function LiveYoutubeUnmannedView({
}, [currentProc, urlAutoRefresh, pullUrlConfigFromServer]);
const reloadUrlManual = async () => {
if (!currentProc) return;
const targetProc = currentProcRef.current.trim();
if (!targetProc) return;
setUrlReloading(true);
setUrlFetchNote(null);
urlListDirtyRef.current = false;
const requestId = ++urlRequestSeqRef.current;
try {
const data = await fetchJson<{ content?: string }>(
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
`/get_url_config?process=${encodeURIComponent(targetProc)}`,
);
if (requestId !== urlRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
const c = data.content ?? "";
if (urlListDirtyRef.current) {
setUrlFetchNote(t("检测到本地未保存修改,已保留当前草稿", "Kept the local unsaved draft"));
return;
}
if (mode === "single") setUrlConfig(c);
if (mode === "single") applySingleUrlDraft(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
applyProUrlDraft(localDraft);
setUrlFetchNote(t("服务器暂无已保存 URL,已保留本地草稿", "Server has no saved URLs, kept the local draft"));
return;
}
setProUrlDraft(c);
applyProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
@@ -439,9 +476,12 @@ export function LiveYoutubeUnmannedView({
}
setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server"));
} catch (e) {
if (requestId !== urlRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
setUrlFetchNote((e as Error).message);
} finally {
setUrlReloading(false);
if (requestId === urlRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
setUrlReloading(false);
}
}
};
@@ -552,11 +592,6 @@ export function LiveYoutubeUnmannedView({
);
const active = channels.find((c) => c.id === activeCh) || channels[0];
const activeSavedUrlDraft = useMemo(() => (active?.urlLines ?? "").trim(), [active?.urlLines]);
const activeUrlDirty = useMemo(
() => (active ? proUrlDraft !== (active.urlLines ?? "").trim() : false),
[active, proUrlDraft],
);
const activeLaneDraft = useMemo(
() => (active ? laneDrafts[active.id] ?? laneDraftFromChannel(active) : null),
[active, laneDrafts],
@@ -663,12 +698,14 @@ export function LiveYoutubeUnmannedView({
const setModePersist = useCallback(
(m: "single" | "pro") => {
if (m === "single" && mode === "pro" && active) {
if (activeUrlDirty) {
persistChannels(channels.map((x) => (x.id === active.id ? { ...x, urlLines: proUrlDraft } : x)));
const latestProUrlDraft = proUrlDraftRef.current;
if (latestProUrlDraft !== (active.urlLines ?? "").trim()) {
persistChannels(channels.map((x) => (x.id === active.id ? { ...x, urlLines: latestProUrlDraft } : x)));
}
}
if (m === "pro" && channels.length) {
const first = channels[0];
const latestSingleUrlDraft = singleUrlDraftRef.current.trim();
const youtubePm2Ok = staticYoutubeEntries.some((e) => e.pm2 === currentProc);
const pm2Name =
youtubePm2Ok && currentProc.trim()
@@ -678,14 +715,14 @@ export function LiveYoutubeUnmannedView({
{
...first,
streamKey: ytFields.key.trim() || first.streamKey || "",
urlLines: urlConfig.trim() || first.urlLines || "",
urlLines: latestSingleUrlDraft || first.urlLines || "",
pm2Name,
},
...channels.slice(1),
];
persistChannels(next);
const ch = next.find((c) => c.id === activeCh) ?? next[0];
setProUrlDraft((ch?.urlLines ?? "").trim() || urlConfig.trim());
applyProUrlDraft((ch?.urlLines ?? "").trim() || latestSingleUrlDraft);
}
setMode(m);
try {
@@ -697,11 +734,9 @@ export function LiveYoutubeUnmannedView({
[
mode,
active,
activeUrlDirty,
channels,
proUrlDraft,
applyProUrlDraft,
ytFields.key,
urlConfig,
currentProc,
staticYoutubeEntries,
activeCh,
@@ -711,15 +746,16 @@ export function LiveYoutubeUnmannedView({
const selectProChannel = useCallback(
(c: YoutubeProChannel) => {
if (active && mode === "pro" && activeUrlDirty) {
persistChannels(channels.map((x) => (x.id === active.id ? { ...x, urlLines: proUrlDraft } : x)));
const latestProUrlDraft = proUrlDraftRef.current;
if (active && mode === "pro" && latestProUrlDraft !== (active.urlLines ?? "").trim()) {
persistChannels(channels.map((x) => (x.id === active.id ? { ...x, urlLines: latestProUrlDraft } : x)));
}
const u = (c.urlLines ?? "").trim();
setProUrlDraft(u);
applyProUrlDraft(u);
setActiveCh(c.id);
setCurrentProc(resolveChannelPm2(c));
},
[active, activeUrlDirty, mode, channels, proUrlDraft, setCurrentProc, persistChannels],
[active, applyProUrlDraft, mode, channels, setCurrentProc, persistChannels],
);
useEffect(() => {
@@ -730,62 +766,36 @@ export function LiveYoutubeUnmannedView({
setCurrentProc(want);
}, [mode, activeCh, channels, setCurrentProc]);
useEffect(() => {
if (mode !== "pro" || !currentProc || !activeCh) return;
let cancelled = false;
const targetChannelId = activeCh;
void (async () => {
try {
const data = await fetchJson<{ content?: string }>(
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
);
if (cancelled || urlListDirtyRef.current) return;
const c = data.content ?? "";
const localDraft = channels.find((row) => row.id === targetChannelId)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
return;
}
setProUrlDraft(c);
urlListDirtyRef.current = false;
setChannels((prev) => {
const next = prev.map((x) => (x.id === targetChannelId ? { ...x, urlLines: c } : x));
saveYoutubeProChannels(next);
return next;
});
} catch {
if (!cancelled && !urlListDirtyRef.current) {
setProUrlDraft(activeSavedUrlDraft);
}
}
})();
return () => {
cancelled = true;
};
}, [activeCh, activeSavedUrlDraft, channels, currentProc, fetchJson, mode]);
const loadYoutubeIni = useCallback(async () => {
if (!currentProc) return;
const targetProc = currentProcRef.current.trim();
if (!targetProc) return;
const requestId = ++ytIniRequestSeqRef.current;
setYtIniLoading(true);
setYtIniStatus(null);
try {
const data = await fetchJson<{ content?: string }>(
`/get_config?process=${encodeURIComponent(currentProc)}`,
`/get_config?process=${encodeURIComponent(targetProc)}`,
);
if (requestId !== ytIniRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
const text = data.content ?? "";
setYoutubeIniText(text);
setYtFields(parseYoutubeIniFields(text));
} catch (e) {
if (requestId !== ytIniRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
setYtIniStatus((e as Error).message);
} finally {
setYtIniLoading(false);
if (requestId === ytIniRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
setYtIniLoading(false);
}
}
}, [currentProc, fetchJson]);
}, [fetchJson]);
useEffect(() => {
if (!currentProc) return;
void pullUrlConfigFromServer();
void loadYoutubeIni();
}, [currentProc, loadYoutubeIni]);
setActiveViewRefreshToken((prev) => prev + 1);
}, [activeCh, currentProc, loadYoutubeIni, mode, pullUrlConfigFromServer]);
const persistActiveLaneDraft = useCallback(
async () => {
@@ -795,6 +805,7 @@ export function LiveYoutubeUnmannedView({
if (!activeLaneValidation.ok) {
throw new Error(activeLaneValidation.message);
}
const latestProUrlDraft = proUrlDraftRef.current;
const next = channels.map((row) =>
row.id === active.id
? {
@@ -803,7 +814,7 @@ export function LiveYoutubeUnmannedView({
streamKey: activeLaneValidation.normalizedStreamKey,
pm2Name: activeLaneValidation.normalizedPm2,
notes: activeLaneValidation.normalizedNotes,
urlLines: proUrlDraft,
urlLines: latestProUrlDraft,
}
: row,
);
@@ -828,7 +839,7 @@ export function LiveYoutubeUnmannedView({
streamKey: activeLaneValidation.normalizedStreamKey,
pm2Name: activeLaneValidation.normalizedPm2,
notes: activeLaneValidation.normalizedNotes,
urlLines: proUrlDraft,
urlLines: latestProUrlDraft,
} satisfies YoutubeProChannel),
pm2: activeLaneValidation.normalizedPm2,
};
@@ -839,7 +850,6 @@ export function LiveYoutubeUnmannedView({
activeLaneValidation,
applyChannelsLocal,
channels,
proUrlDraft,
syncChannelsRemote,
t,
],
@@ -933,9 +943,10 @@ export function LiveYoutubeUnmannedView({
try {
setLaneSaving(true);
setLaneStatus(null);
const latestProUrlDraft = proUrlDraftRef.current;
const saved = await persistActiveLaneDraft();
setCurrentProc(saved.pm2);
await Promise.resolve(onSaveUrlConfig(proUrlDraft, saved.pm2));
await Promise.resolve(onSaveUrlConfig(latestProUrlDraft, saved.pm2));
urlListDirtyRef.current = false;
setLaneStatus(t("线路与地址已保存", "Lane settings and URLs saved"));
} catch (e) {
@@ -948,8 +959,13 @@ export function LiveYoutubeUnmannedView({
};
const saveSingleUrlAndClearDirty = async () => {
const targetPm2 = currentProc.trim();
if (!targetPm2) {
notify(t("请选择进程", "Pick a process"));
return;
}
try {
await Promise.resolve(onSaveUrlConfig());
await Promise.resolve(onSaveUrlConfig(singleUrlDraftRef.current, targetPm2));
urlListDirtyRef.current = false;
} catch {
/* parent 已 toast */
@@ -957,7 +973,7 @@ export function LiveYoutubeUnmannedView({
};
const dedupeUrlDraft = async () => {
const source = mode === "single" ? urlConfig : proUrlDraft;
const source = mode === "single" ? singleUrlDraftRef.current : proUrlDraftRef.current;
setUrlOptimizing(true);
setUrlFetchNote(null);
try {
@@ -972,9 +988,9 @@ export function LiveYoutubeUnmannedView({
}),
});
const next = data.content ?? source;
if (mode === "single") setUrlConfig(next);
if (mode === "single") applySingleUrlDraft(next);
else {
setProUrlDraft(next);
applyProUrlDraft(next);
}
urlListDirtyRef.current = true;
setUrlFetchNote(data.message || t("已去重当前 URL 列表", "Deduplicated current URL list"));
@@ -1574,7 +1590,7 @@ export function LiveYoutubeUnmannedView({
persistChannels([...channels, nc]);
setActiveCh(id);
setCurrentProc(resolveChannelPm2(nc));
setProUrlDraft("");
applyProUrlDraft("");
}}
>
<Plus className="h-3.5 w-3.5" />
@@ -1692,7 +1708,7 @@ export function LiveYoutubeUnmannedView({
value={proUrlDraft}
onChange={(e) => {
urlListDirtyRef.current = true;
setProUrlDraft(e.target.value);
applyProUrlDraft(e.target.value);
}}
spellCheck={false}
placeholder="https://live.douyin.com/..."
@@ -1732,7 +1748,7 @@ export function LiveYoutubeUnmannedView({
value={urlConfig}
onChange={(e) => {
urlListDirtyRef.current = true;
setUrlConfig(e.target.value);
applySingleUrlDraft(e.target.value);
}}
spellCheck={false}
/>
@@ -1755,6 +1771,7 @@ export function LiveYoutubeUnmannedView({
keySuffixUi={keySuffixUi}
showKeyRow
pollMs={800}
refreshToken={activeViewRefreshToken}
/>
</div>
);