s
This commit is contained in:
@@ -224,6 +224,8 @@ export default function LiveControlApp() {
|
||||
const [showStreamProbes, setShowStreamProbes] = useState(true);
|
||||
const dashboardInFlightRef = useRef<Promise<void> | null>(null);
|
||||
const processMonitorInFlightRef = useRef<Promise<void> | null>(null);
|
||||
const currentProcRef = useRef(currentProc);
|
||||
const urlConfigRequestSeqRef = useRef(0);
|
||||
|
||||
const t = useCallback((zh: string, en: string) => tr(lang, zh, en), [lang]);
|
||||
|
||||
@@ -345,13 +347,20 @@ export default function LiveControlApp() {
|
||||
return () => clearInterval(id);
|
||||
}, [autoRefreshEnabled, refreshProcessMonitor, view]);
|
||||
|
||||
useEffect(() => {
|
||||
currentProcRef.current = currentProc;
|
||||
}, [currentProc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc || view !== "live_youtube") return;
|
||||
void (async () => {
|
||||
const targetProc = currentProcRef.current.trim();
|
||||
const requestId = ++urlConfigRequestSeqRef.current;
|
||||
try {
|
||||
const data = await fetchJson<{ content?: string }>(
|
||||
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
|
||||
`/get_url_config?process=${encodeURIComponent(targetProc)}`,
|
||||
);
|
||||
if (requestId !== urlConfigRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
|
||||
setUrlConfig(data.content || "");
|
||||
} catch {
|
||||
/* 保留当前草稿,避免临时读取失败把编辑器清空 */
|
||||
|
||||
@@ -51,6 +51,7 @@ type Props = {
|
||||
keySuffixUi: string;
|
||||
showKeyRow?: boolean;
|
||||
pollMs?: number;
|
||||
refreshToken?: number;
|
||||
};
|
||||
|
||||
function businessLine(t: TFn, raw: string | undefined, note: string | undefined): string {
|
||||
@@ -75,6 +76,7 @@ export function LiveProcessLiveLogCard({
|
||||
keySuffixUi,
|
||||
showKeyRow = true,
|
||||
pollMs = 1000,
|
||||
refreshToken = 0,
|
||||
}: Props) {
|
||||
const [processStatus, setProcessStatus] = useState<string>("");
|
||||
const [recentLog, setRecentLog] = useState("");
|
||||
@@ -91,6 +93,8 @@ export function LiveProcessLiveLogCard({
|
||||
const prevOnlineRef = useRef(false);
|
||||
const pullInFlightRef = useRef(false);
|
||||
const pullQueuedRef = useRef(false);
|
||||
const currentProcRef = useRef(currentProc);
|
||||
const requestSeqRef = useRef(0);
|
||||
|
||||
const douyinHints = extractDouyinRoomHints(urlListText);
|
||||
const onlineNow = /^(online|running)$/i.test(processStatus);
|
||||
@@ -100,16 +104,19 @@ export function LiveProcessLiveLogCard({
|
||||
const primaryUrl = primaryRoomId ? `https://live.douyin.com/${primaryRoomId}` : fallbackUrl || null;
|
||||
|
||||
const pull = useCallback(async () => {
|
||||
if (!currentProc) return;
|
||||
const targetProc = currentProcRef.current.trim();
|
||||
if (!targetProc) return;
|
||||
if (pullInFlightRef.current) {
|
||||
pullQueuedRef.current = true;
|
||||
return;
|
||||
}
|
||||
pullInFlightRef.current = true;
|
||||
const requestId = ++requestSeqRef.current;
|
||||
try {
|
||||
const data = await fetchJson<StatusPayload>(
|
||||
`/status?process=${encodeURIComponent(currentProc)}`,
|
||||
`/status?process=${encodeURIComponent(targetProc)}`,
|
||||
);
|
||||
if (requestId !== requestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
|
||||
const ps = data.process_status || "unknown";
|
||||
setProcessStatus(ps);
|
||||
setRecentLog(data.recent_log || "");
|
||||
@@ -127,6 +134,7 @@ export function LiveProcessLiveLogCard({
|
||||
}
|
||||
prevOnlineRef.current = online;
|
||||
} catch {
|
||||
if (requestId !== requestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
|
||||
setProcessStatus("error");
|
||||
setBusinessStatus("error");
|
||||
setBusinessNote("");
|
||||
@@ -139,11 +147,40 @@ export function LiveProcessLiveLogCard({
|
||||
window.setTimeout(() => void pull(), 0);
|
||||
}
|
||||
}
|
||||
}, [currentProc, fetchJson]);
|
||||
}, [fetchJson]);
|
||||
|
||||
useEffect(() => {
|
||||
currentProcRef.current = currentProc;
|
||||
requestSeqRef.current += 1;
|
||||
pullQueuedRef.current = false;
|
||||
if (!currentProc) {
|
||||
setProcessStatus("");
|
||||
setRecentLog("");
|
||||
setRecentError("");
|
||||
setBusinessStatus("");
|
||||
setBusinessNote("");
|
||||
setFfmpegPids([]);
|
||||
setLogAgeSeconds(null);
|
||||
setLiveSince(null);
|
||||
prevOnlineRef.current = false;
|
||||
return;
|
||||
}
|
||||
setProcessStatus("");
|
||||
setRecentLog("");
|
||||
setRecentError("");
|
||||
setBusinessStatus("");
|
||||
setBusinessNote("");
|
||||
setFfmpegPids([]);
|
||||
setLogAgeSeconds(null);
|
||||
setLiveSince(null);
|
||||
prevOnlineRef.current = false;
|
||||
void pull();
|
||||
}, [pull]);
|
||||
}, [currentProc, pull]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc) return;
|
||||
void pull();
|
||||
}, [currentProc, pull, refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || !currentProc) return;
|
||||
@@ -190,6 +227,9 @@ export function LiveProcessLiveLogCard({
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-white">{t("当前直播日志", "Live process log")}</h3>
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 font-mono text-[10px] text-zinc-300 ring-1 ring-white/10">
|
||||
{currentProc || "—"}
|
||||
</span>
|
||||
<span className="rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-medium text-emerald-200/90 ring-1 ring-emerald-500/25">
|
||||
{paused ? t("已暂停刷新", "Paused") : t("每秒刷新", "~1s refresh")}
|
||||
</span>
|
||||
|
||||
@@ -120,9 +120,18 @@ export function LiveTiktokHdmiView({
|
||||
const [urlAutoRefresh, setUrlAutoRefresh] = useState(true);
|
||||
const [urlFetchNote, setUrlFetchNote] = useState<string | null>(null);
|
||||
const urlListDirtyRef = useRef(false);
|
||||
const urlDraftRef = useRef(urlConfig);
|
||||
const [hdmiNotes, setHdmiNotes] = useState("");
|
||||
const [checks, setChecks] = useState<Record<string, boolean>>({});
|
||||
|
||||
const applyUrlDraft = useCallback(
|
||||
(next: string) => {
|
||||
urlDraftRef.current = next;
|
||||
setUrlConfig(next);
|
||||
},
|
||||
[setUrlConfig],
|
||||
);
|
||||
|
||||
const setSourceModePersist = useCallback(
|
||||
(m: TiktokSourceMode) => {
|
||||
setSourceMode(m);
|
||||
@@ -151,17 +160,21 @@ export function LiveTiktokHdmiView({
|
||||
urlListDirtyRef.current = false;
|
||||
}, [currentProc]);
|
||||
|
||||
useEffect(() => {
|
||||
urlDraftRef.current = urlConfig;
|
||||
}, [urlConfig]);
|
||||
|
||||
const pullUrlConfigFromServer = useCallback(async () => {
|
||||
if (!currentProc) return;
|
||||
try {
|
||||
const data = await fetchJson<{ content?: string }>(
|
||||
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
|
||||
);
|
||||
setUrlConfig(data.content ?? "");
|
||||
applyUrlDraft(data.content ?? "");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [currentProc, fetchJson, setUrlConfig]);
|
||||
}, [applyUrlDraft, currentProc, fetchJson]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProc || !urlAutoRefresh || sourceMode !== "ffmpeg") return;
|
||||
@@ -239,7 +252,7 @@ export function LiveTiktokHdmiView({
|
||||
const data = await fetchJson<{ content?: string }>(
|
||||
`/get_url_config?process=${encodeURIComponent(currentProc)}`,
|
||||
);
|
||||
setUrlConfig(data.content ?? "");
|
||||
applyUrlDraft(data.content ?? "");
|
||||
setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server"));
|
||||
} catch (e) {
|
||||
setUrlFetchNote((e as Error).message);
|
||||
@@ -250,7 +263,7 @@ export function LiveTiktokHdmiView({
|
||||
|
||||
const saveUrlAndClearDirty = async () => {
|
||||
try {
|
||||
await Promise.resolve(onSaveUrlConfig());
|
||||
await Promise.resolve(onSaveUrlConfig(urlDraftRef.current));
|
||||
urlListDirtyRef.current = false;
|
||||
} catch {
|
||||
/* parent 已 toast */
|
||||
@@ -745,7 +758,7 @@ export function LiveTiktokHdmiView({
|
||||
value={urlConfig}
|
||||
onChange={(e) => {
|
||||
urlListDirtyRef.current = true;
|
||||
setUrlConfig(e.target.value);
|
||||
applyUrlDraft(e.target.value);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user