Bughunt: harden async UI flows and optimize large-queue rendering
This commit is contained in:
parent
c83fa3b86a
commit
f5e020da4e
@ -160,6 +160,29 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const packageOrderKey = useMemo(() => snapshot.session.packageOrder.join("|"), [snapshot.session.packageOrder]);
|
const packageOrderKey = useMemo(() => snapshot.session.packageOrder.join("|"), [snapshot.session.packageOrder]);
|
||||||
|
|
||||||
|
const packagePosition = useMemo(() => {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
snapshot.session.packageOrder.forEach((id, index) => {
|
||||||
|
map.set(id, index);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [packageOrderKey]);
|
||||||
|
|
||||||
|
const itemsByPackage = useMemo(() => {
|
||||||
|
const map = new Map<string, DownloadItem[]>();
|
||||||
|
for (const packageId of snapshot.session.packageOrder) {
|
||||||
|
const pkg = snapshot.session.packages[packageId];
|
||||||
|
if (!pkg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const items = pkg.itemIds
|
||||||
|
.map((id) => snapshot.session.items[id])
|
||||||
|
.filter(Boolean) as DownloadItem[];
|
||||||
|
map.set(packageId, items);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [packageOrderKey, snapshot.session.items, snapshot.session.packages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCollapsedPackages((prev) => {
|
setCollapsedPackages((prev) => {
|
||||||
const next: Record<string, boolean> = {};
|
const next: Record<string, boolean> = {};
|
||||||
@ -265,41 +288,51 @@ export function App(): ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSaveSettings = async (): Promise<void> => {
|
const onSaveSettings = async (): Promise<void> => {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.updateSettings(normalizedSettingsDraft);
|
const result = await window.rd.updateSettings(normalizedSettingsDraft);
|
||||||
setSettingsDraft(result);
|
setSettingsDraft(result);
|
||||||
setSettingsDirty(false);
|
setSettingsDirty(false);
|
||||||
applyTheme(result.theme);
|
applyTheme(result.theme);
|
||||||
showToast("Einstellungen gespeichert", 1800);
|
showToast("Einstellungen gespeichert", 1800);
|
||||||
} catch (error) { showToast(`Einstellungen konnten nicht gespeichert werden: ${String(error)}`, 2800); }
|
}, (error) => {
|
||||||
|
showToast(`Einstellungen konnten nicht gespeichert werden: ${String(error)}`, 2800);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCheckUpdates = async (): Promise<void> => {
|
const onCheckUpdates = async (): Promise<void> => {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.checkUpdates();
|
const result = await window.rd.checkUpdates();
|
||||||
await handleUpdateResult(result, "manual");
|
await handleUpdateResult(result, "manual");
|
||||||
} catch (error) { showToast(`Update-Check fehlgeschlagen: ${String(error)}`, 2800); }
|
}, (error) => {
|
||||||
|
showToast(`Update-Check fehlgeschlagen: ${String(error)}`, 2800);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddLinks = async (): Promise<void> => {
|
const onAddLinks = async (): Promise<void> => {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
await window.rd.updateSettings(normalizedSettingsDraft);
|
await window.rd.updateSettings(normalizedSettingsDraft);
|
||||||
const result = await window.rd.addLinks({ rawText: currentCollectorTab.text, packageName: settingsDraft.packageName });
|
const result = await window.rd.addLinks({ rawText: currentCollectorTab.text, packageName: settingsDraft.packageName });
|
||||||
if (result.addedLinks > 0) {
|
if (result.addedLinks > 0) {
|
||||||
showToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
|
showToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
|
||||||
setCollectorTabs((prev) => prev.map((t) => t.id === currentCollectorTab.id ? { ...t, text: "" } : t));
|
setCollectorTabs((prev) => prev.map((t) => t.id === currentCollectorTab.id ? { ...t, text: "" } : t));
|
||||||
} else { showToast("Keine gültigen Links gefunden"); }
|
} else {
|
||||||
} catch (error) { showToast(`Fehler beim Hinzufügen: ${String(error)}`, 2600); }
|
showToast("Keine gültigen Links gefunden");
|
||||||
|
}
|
||||||
|
}, (error) => {
|
||||||
|
showToast(`Fehler beim Hinzufügen: ${String(error)}`, 2600);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImportDlc = async (): Promise<void> => {
|
const onImportDlc = async (): Promise<void> => {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const files = await window.rd.pickContainers();
|
const files = await window.rd.pickContainers();
|
||||||
if (files.length === 0) { return; }
|
if (files.length === 0) { return; }
|
||||||
await window.rd.updateSettings(normalizedSettingsDraft);
|
await window.rd.updateSettings(normalizedSettingsDraft);
|
||||||
const result = await window.rd.addContainers(files);
|
const result = await window.rd.addContainers(files);
|
||||||
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
} catch (error) { showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600); }
|
}, (error) => {
|
||||||
|
showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDrop = async (event: DragEvent<HTMLElement>): Promise<void> => {
|
const onDrop = async (event: DragEvent<HTMLElement>): Promise<void> => {
|
||||||
@ -311,11 +344,13 @@ export function App(): ReactElement {
|
|||||||
const dlc = files.filter((f) => f.name.toLowerCase().endsWith(".dlc")).map((f) => (f as unknown as { path?: string }).path).filter((v): v is string => !!v);
|
const dlc = files.filter((f) => f.name.toLowerCase().endsWith(".dlc")).map((f) => (f as unknown as { path?: string }).path).filter((v): v is string => !!v);
|
||||||
const droppedText = event.dataTransfer.getData("text/plain") || event.dataTransfer.getData("text/uri-list") || "";
|
const droppedText = event.dataTransfer.getData("text/plain") || event.dataTransfer.getData("text/uri-list") || "";
|
||||||
if (dlc.length > 0) {
|
if (dlc.length > 0) {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
await window.rd.updateSettings(normalizedSettingsDraft);
|
await window.rd.updateSettings(normalizedSettingsDraft);
|
||||||
const result = await window.rd.addContainers(dlc);
|
const result = await window.rd.addContainers(dlc);
|
||||||
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
} catch (error) { showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600); }
|
}, (error) => {
|
||||||
|
showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600);
|
||||||
|
});
|
||||||
} else if (droppedText.trim()) {
|
} else if (droppedText.trim()) {
|
||||||
setCollectorTabs((prev) => prev.map((t) => t.id === currentCollectorTab.id
|
setCollectorTabs((prev) => prev.map((t) => t.id === currentCollectorTab.id
|
||||||
? { ...t, text: t.text ? `${t.text}\n${droppedText}` : droppedText } : t));
|
? { ...t, text: t.text ? `${t.text}\n${droppedText}` : droppedText } : t));
|
||||||
@ -325,7 +360,7 @@ export function App(): ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onExportQueue = async (): Promise<void> => {
|
const onExportQueue = async (): Promise<void> => {
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const json = await window.rd.exportQueue();
|
const json = await window.rd.exportQueue();
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@ -335,27 +370,33 @@ export function App(): ReactElement {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
showToast("Queue exportiert");
|
showToast("Queue exportiert");
|
||||||
} catch (error) { showToast(`Export fehlgeschlagen: ${String(error)}`, 2600); }
|
}, (error) => {
|
||||||
|
showToast(`Export fehlgeschlagen: ${String(error)}`, 2600);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImportQueue = async (): Promise<void> => {
|
const onImportQueue = async (): Promise<void> => {
|
||||||
try {
|
if (actionBusyRef.current) {
|
||||||
const input = document.createElement("input");
|
return;
|
||||||
input.type = "file";
|
}
|
||||||
input.accept = ".json";
|
|
||||||
input.onchange = async () => {
|
const input = document.createElement("input");
|
||||||
const file = input.files?.[0];
|
input.type = "file";
|
||||||
if (!file) { return; }
|
input.accept = ".json";
|
||||||
try {
|
input.onchange = async () => {
|
||||||
const text = await file.text();
|
const file = input.files?.[0];
|
||||||
const result = await window.rd.importQueue(text);
|
if (!file) {
|
||||||
showToast(`Importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
return;
|
||||||
} catch (error) {
|
}
|
||||||
showToast(`Import fehlgeschlagen: ${String(error)}`, 2600);
|
await performQuickAction(async () => {
|
||||||
}
|
const text = await file.text();
|
||||||
};
|
const result = await window.rd.importQueue(text);
|
||||||
input.click();
|
showToast(`Importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
} catch (error) { showToast(`Import fehlgeschlagen: ${String(error)}`, 2600); }
|
}, (error) => {
|
||||||
|
showToast(`Import fehlgeschlagen: ${String(error)}`, 2600);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBool = (key: keyof AppSettings, value: boolean): void => {
|
const setBool = (key: keyof AppSettings, value: boolean): void => {
|
||||||
@ -376,7 +417,10 @@ export function App(): ReactElement {
|
|||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const performQuickAction = async (action: () => Promise<unknown>): Promise<void> => {
|
const performQuickAction = async (
|
||||||
|
action: () => Promise<unknown>,
|
||||||
|
onError?: (error: unknown) => void
|
||||||
|
): Promise<void> => {
|
||||||
if (actionBusyRef.current) {
|
if (actionBusyRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -385,7 +429,11 @@ export function App(): ReactElement {
|
|||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(`Fehler: ${String(error)}`, 2600);
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
} else {
|
||||||
|
showToast(`Fehler: ${String(error)}`, 2600);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actionBusyRef.current = false;
|
actionBusyRef.current = false;
|
||||||
@ -579,10 +627,10 @@ export function App(): ReactElement {
|
|||||||
<div className="collector-header">
|
<div className="collector-header">
|
||||||
<h3>Linksammler</h3>
|
<h3>Linksammler</h3>
|
||||||
<div className="link-actions">
|
<div className="link-actions">
|
||||||
<button className="btn" onClick={onImportDlc}>DLC import</button>
|
<button className="btn" disabled={actionBusy} onClick={onImportDlc}>DLC import</button>
|
||||||
<button className="btn" onClick={onExportQueue}>Queue Export</button>
|
<button className="btn" disabled={actionBusy} onClick={onExportQueue}>Queue Export</button>
|
||||||
<button className="btn" onClick={onImportQueue}>Queue Import</button>
|
<button className="btn" disabled={actionBusy} onClick={onImportQueue}>Queue Import</button>
|
||||||
<button className="btn accent" onClick={onAddLinks}>Zur Queue hinzufügen</button>
|
<button className="btn accent" disabled={actionBusy} onClick={onAddLinks}>Zur Queue hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="collector-tabs">
|
<div className="collector-tabs">
|
||||||
@ -648,10 +696,10 @@ export function App(): ReactElement {
|
|||||||
<PackageCard
|
<PackageCard
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)}
|
items={itemsByPackage.get(pkg.id) ?? []}
|
||||||
packageSpeed={packageSpeedMap.get(pkg.id) ?? 0}
|
packageSpeed={packageSpeedMap.get(pkg.id) ?? 0}
|
||||||
isFirst={snapshot.session.packageOrder.indexOf(pkg.id) === 0}
|
isFirst={(packagePosition.get(pkg.id) ?? -1) === 0}
|
||||||
isLast={snapshot.session.packageOrder.indexOf(pkg.id) === snapshot.session.packageOrder.length - 1}
|
isLast={(packagePosition.get(pkg.id) ?? -1) === snapshot.session.packageOrder.length - 1}
|
||||||
isEditing={editingPackageId === pkg.id}
|
isEditing={editingPackageId === pkg.id}
|
||||||
editingName={editingName}
|
editingName={editingName}
|
||||||
collapsed={collapsedPackages[pkg.id] ?? false}
|
collapsed={collapsedPackages[pkg.id] ?? false}
|
||||||
@ -682,7 +730,7 @@ export function App(): ReactElement {
|
|||||||
<span>Kompakt, schnell auffindbar und direkt speicherbar.</span>
|
<span>Kompakt, schnell auffindbar und direkt speicherbar.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-toolbar-actions">
|
<div className="settings-toolbar-actions">
|
||||||
<button className="btn" onClick={onCheckUpdates}>Updates prüfen</button>
|
<button className="btn" disabled={actionBusy} onClick={onCheckUpdates}>Updates prüfen</button>
|
||||||
<button className={`btn${settingsDraft.theme === "light" ? " btn-active" : ""}`} onClick={() => {
|
<button className={`btn${settingsDraft.theme === "light" ? " btn-active" : ""}`} onClick={() => {
|
||||||
const next = settingsDraft.theme === "dark" ? "light" : "dark";
|
const next = settingsDraft.theme === "dark" ? "light" : "dark";
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
@ -691,7 +739,7 @@ export function App(): ReactElement {
|
|||||||
}}>
|
}}>
|
||||||
{settingsDraft.theme === "dark" ? "Light Mode" : "Dark Mode"}
|
{settingsDraft.theme === "dark" ? "Light Mode" : "Dark Mode"}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn accent" onClick={onSaveSettings}>Einstellungen speichern</button>
|
<button className="btn accent" disabled={actionBusy} onClick={onSaveSettings}>Einstellungen speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user