Complete provider waits and lifecycle visibility
Release aborted Real-Debrid, AllDebrid, and BestDebrid web callers even when underlying requests ignore cancellation while retaining terminal rejection observers. Publish provider cooldown deadlines and emit a fresh idle snapshot at the earliest expiry. Abort and visibly drain post-processing before dispatching one pending restart. Surface lifecycle phase, reason, retry countdown, and remaining work in the download controls. Add focused regressions for provider abort races, cooldown expiry, post-processing drain, pending start visibility, and the updated rapid stop contract.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { ReactElement } from "react";
|
||||
import type { DownloadLifecycleSnapshot } from "../../../shared/types";
|
||||
import { RollingMetricValue } from "../../ui/RollingMetricValue";
|
||||
import { SlidingSelection } from "../../ui/SlidingSelection";
|
||||
import type { DownloadsViewModelCore, DownloadDisplayMode, DownloadSidebarFilter } from "./downloads-model";
|
||||
@@ -54,8 +55,10 @@ export interface DownloadsViewModel extends DownloadsViewModelCore {
|
||||
sortDirection?: "asc" | "desc";
|
||||
disclosureRevision: number;
|
||||
animationsEnabled: boolean;
|
||||
lifecycle: DownloadLifecycleSnapshot;
|
||||
lifecycleNow: number;
|
||||
status: DownloadsStatusModel;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DownloadsViewActions extends DownloadsTableActions {
|
||||
onResetColumnLayout: () => void;
|
||||
@@ -134,7 +137,7 @@ export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewAct
|
||||
const scheduleSlotClass = `downloads-schedule-slot ${scheduleSlotOpen ? "is-open" : "is-closed"}${model.animationsEnabled ? "" : " is-motion-disabled"}`;
|
||||
return (
|
||||
<div className="downloads-toolbar" data-visual-region="downloads-toolbar">
|
||||
<button disabled={model.actionBusy || !model.canStart} onClick={actions.onStartDownloads} type="button">Start</button>
|
||||
<button disabled={model.actionBusy || !model.canStart} onClick={actions.onStartDownloads} type="button">{model.lifecycle.pendingStart ? "Start vorgemerkt" : "Start"}</button>
|
||||
<button disabled={!model.canPause || model.paused} onClick={actions.onPauseDownloads} type="button">Pause</button>
|
||||
<button disabled={!model.canStop || model.actionBusy} onClick={actions.onStopDownloads} type="button">Stop</button>
|
||||
{!model.scheduleActive ? <button aria-expanded={model.scheduleOpen} onClick={actions.onToggleSchedule} type="button">Zeitplan</button> : null}
|
||||
@@ -171,13 +174,42 @@ export function DownloadsContent({ actions, model }: { actions: DownloadsViewAct
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function formatLifecycleStatus(model: DownloadsViewModel): string {
|
||||
const phaseLabels: Record<DownloadLifecycleSnapshot["phase"], string> = {
|
||||
idle: "Bereit",
|
||||
starting: "Startet",
|
||||
running: "Läuft",
|
||||
stopping: "Stoppt",
|
||||
waiting_provider: "Wartet auf Provider",
|
||||
postprocessing: "Nachbearbeitung"
|
||||
};
|
||||
const parts = [phaseLabels[model.lifecycle.phase]];
|
||||
if (model.lifecycle.reason && model.lifecycle.reason !== parts[0]) {
|
||||
parts.push(model.lifecycle.reason);
|
||||
}
|
||||
if (model.lifecycle.retryAt && model.lifecycle.retryAt > model.lifecycleNow) {
|
||||
parts.push(`Noch ${Math.max(1, Math.ceil((model.lifecycle.retryAt - model.lifecycleNow) / 1000))} s`);
|
||||
}
|
||||
const remaining: string[] = [];
|
||||
if (model.lifecycle.activeDownloads > 0) {
|
||||
remaining.push(`${model.lifecycle.activeDownloads} ${model.lifecycle.activeDownloads === 1 ? "Download" : "Downloads"}`);
|
||||
}
|
||||
if (model.lifecycle.activePostProcessing > 0) {
|
||||
remaining.push(`${model.lifecycle.activePostProcessing} ${model.lifecycle.activePostProcessing === 1 ? "Nachbearbeitung" : "Nachbearbeitungen"}`);
|
||||
}
|
||||
if (remaining.length > 0) {
|
||||
parts.push(`Restarbeit: ${remaining.join(", ")}`);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
export function DownloadsFooter({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
|
||||
export function DownloadsFooter({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
|
||||
return (
|
||||
<footer className="downloads-footer" data-visual-region="downloads-pagination">
|
||||
<span>{model.paginationLabel}</span>
|
||||
{model.limited ? <button onClick={actions.onShowAllPackages} type="button">Alle anzeigen</button> : null}
|
||||
<span>{model.running ? model.paused ? "Pausiert" : "Download läuft" : "Bereit"}</span>
|
||||
<span role="status">{formatLifecycleStatus(model)}</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user