Improve collector metadata progress and alignment
Stream completed metadata checks into the collector while preserving generation-based stale-result protection. Derive row statuses from verified availability, move analysis feedback into the sidebar, center table values under their headers, and keep the header synchronized during horizontal scrolling.
This commit is contained in:
@@ -1001,8 +1001,11 @@ export class AppController {
|
||||
return prepareCollectorContainers(filePaths, addedAt);
|
||||
}
|
||||
|
||||
public enrichCollectorPackages(request: CollectorEnrichmentRequest): Promise<CollectorInspectionResult> {
|
||||
return enrichCollectorPackages(request, this.settings);
|
||||
public enrichCollectorPackages(
|
||||
request: CollectorEnrichmentRequest,
|
||||
onProgress?: (result: CollectorInspectionResult) => void
|
||||
): Promise<CollectorInspectionResult> {
|
||||
return enrichCollectorPackages(request, this.settings, {}, onProgress);
|
||||
}
|
||||
|
||||
public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> {
|
||||
|
||||
@@ -27,10 +27,42 @@ export interface CollectorInspectionDependencies {
|
||||
checkDdownload?: typeof checkDdownloadOnline;
|
||||
checkOneFichier?: (links: string[]) => Promise<Map<string, OneFichierCheckResult>>;
|
||||
checkRapidgator?: typeof checkRapidgatorOnline;
|
||||
resolveFilenames?: (links: string[]) => Promise<Map<string, string>>;
|
||||
resolveFilenames?: (links: string[], onResolved?: (link: string, fileName: string) => void) => Promise<Map<string, string>>;
|
||||
importContainers?: typeof importDlcContainers;
|
||||
}
|
||||
|
||||
function collectorProgressResult(packages: CollectorPackage[], urls: ReadonlySet<string>): CollectorInspectionResult {
|
||||
const fragments = packages.flatMap((pkg) => {
|
||||
const links = pkg.links.filter((link) => urls.has(link.url)).map((link) => ({ ...link }));
|
||||
return links.length > 0 ? [{ ...pkg, links }] : [];
|
||||
});
|
||||
return { packages: regroupEnrichedPackages(fragments), invalidCount: 0, duplicateCount: 0 };
|
||||
}
|
||||
|
||||
function createCollectorProgressEmitter(
|
||||
packages: CollectorPackage[],
|
||||
onProgress?: (result: CollectorInspectionResult) => void
|
||||
): { queue: (url: string) => void; flush: () => void } {
|
||||
const pending = new Set<string>();
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const flush = (): void => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = null;
|
||||
if (!onProgress || pending.size === 0) return;
|
||||
const urls = new Set(pending);
|
||||
pending.clear();
|
||||
onProgress(collectorProgressResult(packages, urls));
|
||||
};
|
||||
return {
|
||||
queue: (url) => {
|
||||
if (!onProgress) return;
|
||||
pending.add(url);
|
||||
if (!timer) timer = setTimeout(flush, 100);
|
||||
},
|
||||
flush
|
||||
};
|
||||
}
|
||||
|
||||
interface PreparedSourceLink {
|
||||
url: string;
|
||||
fileName: string;
|
||||
@@ -177,7 +209,8 @@ function regroupEnrichedPackages(packages: CollectorPackage[]): CollectorPackage
|
||||
export async function enrichCollectorPackages(
|
||||
request: CollectorEnrichmentRequest,
|
||||
settings: AppSettings,
|
||||
dependencies: CollectorInspectionDependencies = {}
|
||||
dependencies: CollectorInspectionDependencies = {},
|
||||
onProgress?: (result: CollectorInspectionResult) => void
|
||||
): Promise<CollectorInspectionResult> {
|
||||
const packages = request.packages.map((pkg) => ({ ...pkg, links: pkg.links.map((link) => ({ ...link })) }));
|
||||
const linksByUrl = new Map(packages.flatMap((pkg) => pkg.links).map((link) => [link.url, link]));
|
||||
@@ -195,9 +228,21 @@ export async function enrichCollectorPackages(
|
||||
const checkOneFichier = dependencies.checkOneFichier ?? checkOneFichierLinks;
|
||||
const checkRapidgator = dependencies.checkRapidgator ?? checkRapidgatorOnline;
|
||||
const checkDdownload = dependencies.checkDdownload ?? checkDdownloadOnline;
|
||||
const resolveFilenames = dependencies.resolveFilenames ?? ((links) => new DebridService(settings).resolveFilenames(links));
|
||||
const resolveFilenames = dependencies.resolveFilenames ?? ((links, onResolved) => new DebridService(settings).resolveFilenames(links, onResolved));
|
||||
const progress = createCollectorProgressEmitter(packages, onProgress);
|
||||
const oneFichierPromise = oneFichierLinks.length > 0
|
||||
? checkOneFichier(oneFichierLinks).catch(() => new Map<string, OneFichierCheckResult>())
|
||||
? checkOneFichier(oneFichierLinks).then((results) => {
|
||||
for (const [url, result] of results) {
|
||||
const link = linksByUrl.get(url);
|
||||
if (!link) continue;
|
||||
link.availability = result.online ? "online" : "offline";
|
||||
link.status = result.online ? (result.fileName ? "ready" : "unknown") : "offline";
|
||||
if (result.fileName) link.fileName = sanitizeFilename(result.fileName);
|
||||
if (result.fileSizeBytes !== null && result.fileSizeBytes >= 0) link.fileSizeBytes = result.fileSizeBytes;
|
||||
progress.queue(url);
|
||||
}
|
||||
return results;
|
||||
}).catch(() => new Map<string, OneFichierCheckResult>())
|
||||
: Promise.resolve(new Map<string, OneFichierCheckResult>());
|
||||
const rapidgatorPromise = runWithConcurrency(rapidgatorLinks, 8, async (url) => {
|
||||
const result = await checkRapidgator(url).catch(() => null);
|
||||
@@ -207,6 +252,7 @@ export async function enrichCollectorPackages(
|
||||
link.status = result.online ? (result.fileName ? "ready" : "unknown") : "offline";
|
||||
if (result.fileName) link.fileName = sanitizeFilename(result.fileName);
|
||||
if (result.fileSizeBytes !== null && result.fileSizeBytes >= 0) link.fileSizeBytes = result.fileSizeBytes;
|
||||
progress.queue(url);
|
||||
});
|
||||
const ddownloadPromise = runWithConcurrency(ddownloadLinks, 4, async (url) => {
|
||||
const result = await checkDdownload(url).catch(() => null);
|
||||
@@ -216,9 +262,16 @@ export async function enrichCollectorPackages(
|
||||
link.status = result.online ? (result.fileName ? "ready" : "unknown") : "offline";
|
||||
if (result.fileName) link.fileName = sanitizeFilename(result.fileName);
|
||||
if (result.fileSizeBytes !== null && result.fileSizeBytes >= 0) link.fileSizeBytes = result.fileSizeBytes;
|
||||
progress.queue(url);
|
||||
});
|
||||
const genericPromise = genericLinks.length > 0
|
||||
? resolveFilenames(genericLinks).catch(() => new Map<string, string>())
|
||||
? resolveFilenames(genericLinks, (url, fileName) => {
|
||||
const link = linksByUrl.get(url);
|
||||
if (!link || !fileName) return;
|
||||
link.fileName = sanitizeFilename(fileName);
|
||||
link.status = "ready";
|
||||
progress.queue(url);
|
||||
}).catch(() => new Map<string, string>())
|
||||
: Promise.resolve(new Map<string, string>());
|
||||
const [oneFichierResults, genericResults] = await Promise.all([
|
||||
oneFichierPromise,
|
||||
@@ -226,20 +279,15 @@ export async function enrichCollectorPackages(
|
||||
rapidgatorPromise,
|
||||
ddownloadPromise
|
||||
]).then(([oneFichier, generic]) => [oneFichier, generic] as const);
|
||||
for (const [url, result] of oneFichierResults) {
|
||||
const link = linksByUrl.get(url);
|
||||
if (!link) continue;
|
||||
link.availability = result.online ? "online" : "offline";
|
||||
link.status = result.online ? (result.fileName ? "ready" : "unknown") : "offline";
|
||||
if (result.fileName) link.fileName = sanitizeFilename(result.fileName);
|
||||
if (result.fileSizeBytes !== null && result.fileSizeBytes >= 0) link.fileSizeBytes = result.fileSizeBytes;
|
||||
}
|
||||
void oneFichierResults;
|
||||
for (const [url, fileName] of genericResults) {
|
||||
const link = linksByUrl.get(url);
|
||||
if (!link || !fileName) continue;
|
||||
link.fileName = sanitizeFilename(fileName);
|
||||
link.status = "ready";
|
||||
progress.queue(url);
|
||||
}
|
||||
progress.flush();
|
||||
return { packages: regroupEnrichedPackages(packages), invalidCount: 0, duplicateCount: 0 };
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -569,8 +569,13 @@ function registerIpcHandlers(): void {
|
||||
const request = validateCollectorContainerPreparationRequest({ filePaths, addedAt });
|
||||
return controller.prepareCollectorContainers(request.filePaths, request.addedAt);
|
||||
});
|
||||
handleTrusted(IPC_CHANNELS.ENRICH_COLLECTOR_PACKAGES, (_event: IpcMainInvokeEvent, value: unknown) => {
|
||||
return controller.enrichCollectorPackages(validateCollectorEnrichmentRequest(value));
|
||||
handleTrusted(IPC_CHANNELS.ENRICH_COLLECTOR_PACKAGES, (event: IpcMainInvokeEvent, value: unknown) => {
|
||||
const request = validateCollectorEnrichmentRequest(value);
|
||||
return controller.enrichCollectorPackages(request, (result) => {
|
||||
if (!event.sender.isDestroyed()) {
|
||||
event.sender.send(IPC_CHANNELS.COLLECTOR_ENRICHMENT_PROGRESS, { requestId: request.requestId, result });
|
||||
}
|
||||
});
|
||||
});
|
||||
handleTrusted(IPC_CHANNELS.GET_START_CONFLICTS, () => controller.getStartConflicts());
|
||||
handleTrusted(IPC_CHANNELS.RESOLVE_START_CONFLICT, (_event: IpcMainInvokeEvent, packageId: string, policy: "keep" | "skip" | "overwrite") => {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import type { RealDebridLoginRequest } from "../shared/preload-api";
|
||||
import type {
|
||||
CollectorEnrichmentRequest,
|
||||
CollectorEnrichmentProgress,
|
||||
CollectorInspectionResult,
|
||||
CollectorTextPreparationRequest
|
||||
} from "../shared/collector";
|
||||
@@ -62,6 +63,11 @@ const api: ElectronApi = {
|
||||
ipcRenderer.invoke(IPC_CHANNELS.PREPARE_COLLECTOR_CONTAINERS, filePaths, addedAt),
|
||||
enrichCollectorPackages: (request: CollectorEnrichmentRequest): Promise<CollectorInspectionResult> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.ENRICH_COLLECTOR_PACKAGES, request),
|
||||
onCollectorEnrichmentProgress: (callback: (progress: CollectorEnrichmentProgress) => void): (() => void) => {
|
||||
const listener = (_event: unknown, progress: CollectorEnrichmentProgress): void => callback(progress);
|
||||
ipcRenderer.on(IPC_CHANNELS.COLLECTOR_ENRICHMENT_PROGRESS, listener);
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.COLLECTOR_ENRICHMENT_PROGRESS, listener);
|
||||
},
|
||||
getPathForDroppedFile: (file: File): string => webUtils.getPathForFile(file),
|
||||
getStartConflicts: (): Promise<StartConflictEntry[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_START_CONFLICTS),
|
||||
resolveStartConflict: (packageId: string, policy: DuplicatePolicy): Promise<StartConflictResolutionResult> =>
|
||||
|
||||
+21
-7
@@ -61,7 +61,7 @@ import { Dialog } from "./ui/Dialog";
|
||||
import { Icon } from "./ui/Icon";
|
||||
import { Toast } from "./ui/Toast";
|
||||
import { LinkAddressesDialog } from "./ui/LinkAddressesDialog";
|
||||
import { serializeCollectorPackages, type CollectorInspectionResult, type CollectorPackage } from "../shared/collector";
|
||||
import { serializeCollectorPackages, type CollectorEnrichmentProgress, type CollectorInspectionResult, type CollectorPackage } from "../shared/collector";
|
||||
import { routeDroppedDlcFiles } from "./collector-drop";
|
||||
import { beginCollectorEnrichment, filterCurrentCollectorEnrichment } from "./collector-enrichment";
|
||||
import {
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
CollectorInputDialog,
|
||||
MemoizedCollectorContent,
|
||||
CollectorSidebar,
|
||||
CollectorSidebarStatus,
|
||||
CollectorToolbar,
|
||||
type CollectorViewActions
|
||||
} from "./views/collector/CollectorView";
|
||||
@@ -1720,6 +1721,7 @@ export function App(): ReactElement {
|
||||
const [collectorInput, setCollectorInput] = useState<CollectorInputState | null>(null);
|
||||
const collectorPackagesRef = useRef<CollectorPackage[]>(collectorPackages);
|
||||
const collectorEnrichmentGenerationsRef = useRef(new Map<string, number>());
|
||||
const collectorEnrichmentRequestsRef = useRef(new Map<string, ReturnType<typeof beginCollectorEnrichment>>());
|
||||
const importCollectorTextRef = useRef<(rawText: string) => Promise<void>>(() => Promise.resolve());
|
||||
const activeTabRef = useRef<Tab>(tab);
|
||||
const packageOrderRef = useRef<string[]>([]);
|
||||
@@ -3608,8 +3610,10 @@ export function App(): ReactElement {
|
||||
return;
|
||||
}
|
||||
const generations = beginCollectorEnrichment(packages, collectorEnrichmentGenerationsRef.current);
|
||||
const requestId = `collector-${Date.now().toString(36)}-${crypto.randomUUID()}`;
|
||||
collectorEnrichmentRequestsRef.current.set(requestId, generations);
|
||||
setCollectorAnalyzingCount((current) => current + 1);
|
||||
void window.rd.enrichCollectorPackages({ packages }).then((result) => {
|
||||
void window.rd.enrichCollectorPackages({ requestId, packages }).then((result) => {
|
||||
mergeCollectorResult({
|
||||
...result,
|
||||
packages: filterCurrentCollectorEnrichment(
|
||||
@@ -3623,10 +3627,24 @@ export function App(): ReactElement {
|
||||
setCollectorError(`Metadatenprüfung fehlgeschlagen: ${String(error)}`);
|
||||
}
|
||||
}).finally(() => {
|
||||
collectorEnrichmentRequestsRef.current.delete(requestId);
|
||||
setCollectorAnalyzingCount((current) => Math.max(0, current - 1));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => window.rd.onCollectorEnrichmentProgress((progress: CollectorEnrichmentProgress) => {
|
||||
const generations = collectorEnrichmentRequestsRef.current.get(progress.requestId);
|
||||
if (!generations) return;
|
||||
mergeCollectorResult({
|
||||
...progress.result,
|
||||
packages: filterCurrentCollectorEnrichment(
|
||||
progress.result.packages,
|
||||
generations,
|
||||
collectorEnrichmentGenerationsRef.current
|
||||
)
|
||||
}, true);
|
||||
}), []);
|
||||
|
||||
const importCollectorText = async (rawText: string): Promise<void> => {
|
||||
if (!rawText.trim()) {
|
||||
showToast("Keine Links eingegeben", 2200);
|
||||
@@ -6245,11 +6263,7 @@ export function App(): ReactElement {
|
||||
sidebarStatus={tab === "downloads" ? (
|
||||
<DownloadsSidebarStatus model={downloadsViewModel} />
|
||||
) : tab === "collector" ? (
|
||||
<>
|
||||
<span>Pakete: {collectorPackages.length}</span>
|
||||
<span>Links: {collectorViewModel.totalCount}</span>
|
||||
<span>Ausgewählt: {collectorViewModel.selectedCount}</span>
|
||||
</>
|
||||
<CollectorSidebarStatus model={collectorViewModel} />
|
||||
) : tab === "history" ? (
|
||||
<>
|
||||
<span>Einträge: {historyViewModel.totalCount}</span>
|
||||
|
||||
@@ -48,10 +48,9 @@ export interface CollectorInputDialogProps {
|
||||
}
|
||||
|
||||
function packageStatus(row: CollectorWorkspacePackageRow): string {
|
||||
const ready = row.allLinks.reduce((count, link) => count + (link.status === "ready" ? 1 : 0), 0);
|
||||
if (row.offlineCount === row.totalCount) return "Offline";
|
||||
if (ready === row.totalCount) return "Bereit";
|
||||
if (ready > 0 || row.onlineCount > 0) return `${ready}/${row.totalCount} geprüft`;
|
||||
if (row.onlineCount === row.totalCount) return "Online";
|
||||
if (row.onlineCount > 0) return "Teilweise online";
|
||||
return "Ungeprüft";
|
||||
}
|
||||
|
||||
@@ -73,10 +72,11 @@ function availabilityClass(row: CollectorWorkspacePackageRow): string {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function linkStatus(status: "ready" | "offline" | "unknown"): string {
|
||||
if (status === "ready") return "Bereit";
|
||||
if (status === "offline") return "Offline";
|
||||
return "Ungeprüft";
|
||||
function availabilityTone(online: number, offline: number, total: number): "online" | "offline" | "partial" | "unknown" {
|
||||
if (online === total) return "online";
|
||||
if (offline === total) return "offline";
|
||||
if (online > 0) return "partial";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function linkAvailability(availability: "online" | "offline" | "unknown"): string {
|
||||
@@ -101,6 +101,7 @@ export function collectorFileInteractionAttributes(
|
||||
}
|
||||
|
||||
interface CollectorViewportState {
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
viewportHeight: number;
|
||||
}
|
||||
@@ -116,6 +117,7 @@ interface CollectorVirtualPackage {
|
||||
|
||||
function useCollectorViewport(bodyRef: React.RefObject<HTMLDivElement>): CollectorViewportState {
|
||||
const [viewport, setViewport] = useState<CollectorViewportState>({
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
viewportHeight: DOWNLOAD_VIRTUAL_DEFAULT_VIEWPORT_HEIGHT
|
||||
});
|
||||
@@ -126,10 +128,11 @@ function useCollectorViewport(bodyRef: React.RefObject<HTMLDivElement>): Collect
|
||||
const measure = (): void => {
|
||||
frame = 0;
|
||||
const next = {
|
||||
scrollLeft: body.scrollLeft,
|
||||
scrollTop: body.scrollTop,
|
||||
viewportHeight: body.clientHeight || DOWNLOAD_VIRTUAL_DEFAULT_VIEWPORT_HEIGHT
|
||||
};
|
||||
setViewport((current) => current.scrollTop === next.scrollTop && current.viewportHeight === next.viewportHeight ? current : next);
|
||||
setViewport((current) => current.scrollLeft === next.scrollLeft && current.scrollTop === next.scrollTop && current.viewportHeight === next.viewportHeight ? current : next);
|
||||
};
|
||||
const schedule = (): void => {
|
||||
if (frame !== 0) return;
|
||||
@@ -147,6 +150,10 @@ function useCollectorViewport(bodyRef: React.RefObject<HTMLDivElement>): Collect
|
||||
return viewport;
|
||||
}
|
||||
|
||||
export function collectorHeaderScrollStyle(scrollLeft: number): CSSProperties {
|
||||
return { transform: `translateX(${-Math.max(0, scrollLeft)}px)` };
|
||||
}
|
||||
|
||||
function collectorVirtualPackageStyle(top: number, height: number): CSSProperties {
|
||||
return {
|
||||
"--collector-virtual-package-top": `${top}px`,
|
||||
@@ -208,6 +215,19 @@ export function CollectorSidebar({ model, actions }: CollectorViewProps): ReactE
|
||||
);
|
||||
}
|
||||
|
||||
export function CollectorSidebarStatus({ model }: { model: CollectorWorkspaceViewModel }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<span>Pakete: {model.packageCount}</span>
|
||||
<span>Links: {model.totalCount}</span>
|
||||
<span>Ausgewählt: {model.selectedCount}</span>
|
||||
{model.analyzing ? (
|
||||
<span aria-live="polite" className="collector-sidebar-analysis" role="status"><span aria-hidden="true" />Analyse läuft im Hintergrund</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CollectorToolbar({ model, actions }: CollectorViewProps): ReactElement {
|
||||
return (
|
||||
<Toolbar className="collector-toolbar" data-visual-region="collector-toolbar" label="Linksammler-Aktionen">
|
||||
@@ -300,7 +320,7 @@ function CollectorPackageGroup({ row, model, actions, selected, focusIndexStart,
|
||||
<span className="collector-hoster-cell" role="cell">
|
||||
{row.hosters.map(formatHosterLabel).map((hoster) => <CollectorHosterLabel hoster={hoster} key={hoster.title} />)}
|
||||
</span>
|
||||
<span className="collector-status-cell" role="cell">{packageStatus(row)}</span>
|
||||
<span className={`collector-status-cell is-${availabilityTone(row.onlineCount, row.offlineCount, row.totalCount)}`} role="cell">{packageStatus(row)}</span>
|
||||
<span className={`collector-availability-cell is-${availabilityClass(row)}`} role="cell">{packageAvailability(row)}</span>
|
||||
<span className="collector-added-cell" role="cell">{formatDateTime(row.addedAt)}</span>
|
||||
</div>
|
||||
@@ -318,7 +338,7 @@ function CollectorPackageGroup({ row, model, actions, selected, focusIndexStart,
|
||||
<span className="collector-name-cell is-file" role="cell" title={link.url}><span className={`collector-link-state is-${link.availability}`} />{link.fileName}</span>
|
||||
<span className="collector-size-cell" role="cell">{link.fileSizeBytes === null ? "Unbekannt" : humanSize(link.fileSizeBytes)}</span>
|
||||
<span className="collector-hoster-cell" role="cell"><CollectorHosterLabel hoster={hoster} /></span>
|
||||
<span className="collector-status-cell" role="cell">{linkStatus(link.status)}</span>
|
||||
<span className={`collector-status-cell is-${link.availability}`} role="cell">{linkAvailability(link.availability)}</span>
|
||||
<span className={`collector-availability-cell is-${link.availability}`} role="cell">{linkAvailability(link.availability)}</span>
|
||||
<span className="collector-added-cell" role="cell">{formatDateTime(link.addedAt)}</span>
|
||||
</div>
|
||||
@@ -368,7 +388,7 @@ export function CollectorContent({ model, actions }: CollectorViewProps): ReactE
|
||||
model.animationsEnabled
|
||||
);
|
||||
const effectivePinnedIds = mergeCollectorPinnedIds(resolveCollectorTransitionPins(transitionPinnedIds, freshPinnedIds), focusedPackageId);
|
||||
const stateOffset = (model.analyzing ? 38 : 0) + (model.error ? 38 : 0);
|
||||
const stateOffset = model.error ? 38 : 0;
|
||||
const virtualWindow = useMemo(() => calculateDownloadVirtualWindow(virtualPackages, {
|
||||
scrollTop: Math.max(0, viewport.scrollTop - stateOffset),
|
||||
viewportHeight: viewport.viewportHeight,
|
||||
@@ -406,7 +426,7 @@ export function CollectorContent({ model, actions }: CollectorViewProps): ReactE
|
||||
<section className="collector-content" aria-label="Gesammelte Downloadpakete">
|
||||
<DataTable aria-rowcount={logicalRowCount} className="collector-table" label="Gesammelte Downloadpakete">
|
||||
<DataTableHeader className="collector-table-header">
|
||||
<div aria-rowindex={1} className="collector-table-header-row" role="row">
|
||||
<div aria-rowindex={1} className="collector-table-header-row" role="row" style={collectorHeaderScrollStyle(viewport.scrollLeft)}>
|
||||
<span aria-label="Auswahl" className="collector-column-select" role="columnheader" />
|
||||
<span role="columnheader">Name</span>
|
||||
<span role="columnheader">Größe</span>
|
||||
@@ -433,7 +453,6 @@ export function CollectorContent({ model, actions }: CollectorViewProps): ReactE
|
||||
}}
|
||||
ref={bodyRef}
|
||||
>
|
||||
{model.analyzing ? <div aria-live="polite" className="collector-background-state" role="status"><span />Analyse läuft im Hintergrund</div> : null}
|
||||
{model.error ? <div aria-live="polite" className="collector-background-error" role="status">{model.error}</div> : null}
|
||||
{model.empty ? (
|
||||
<DataTableEmpty
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface CollectorWorkspacePackageRow {
|
||||
|
||||
export interface CollectorWorkspaceViewModel {
|
||||
packages: CollectorWorkspacePackageRow[];
|
||||
packageCount: number;
|
||||
filters: CollectorWorkspaceFilterEntry[];
|
||||
filter: CollectorWorkspaceFilter;
|
||||
query: string;
|
||||
@@ -276,6 +277,7 @@ export function buildCollectorWorkspaceViewModel(
|
||||
|
||||
return {
|
||||
packages: rows,
|
||||
packageCount: packages.length,
|
||||
filters: [
|
||||
{ id: "all", label: "Alle", count: allLinks.length },
|
||||
{ id: "online", label: "Online", count: availabilityCounts.online },
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.collector-background-state,
|
||||
.collector-background-error {
|
||||
align-items: center;
|
||||
backdrop-filter: blur(8px);
|
||||
@@ -156,14 +155,15 @@
|
||||
padding: 0 11px;
|
||||
}
|
||||
|
||||
.collector-background-state {
|
||||
background: color-mix(in srgb, var(--ui-surface) 88%, transparent);
|
||||
color: var(--ui-text-secondary);
|
||||
.collector-sidebar-analysis {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.collector-background-state span {
|
||||
.collector-sidebar-analysis > span {
|
||||
animation: collector-analysis-pulse 1s ease-in-out infinite alternate;
|
||||
background: var(--ui-primary);
|
||||
background: var(--ui-success);
|
||||
border-radius: 50%;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
@@ -184,6 +184,12 @@
|
||||
|
||||
.collector-table-header {
|
||||
height: 41px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collector-table-header,
|
||||
.collector-table-body {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.collector-table-header-row,
|
||||
@@ -211,6 +217,25 @@
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.collector-table-header-row > span:nth-child(2) {
|
||||
padding-left: 48px;
|
||||
}
|
||||
|
||||
.collector-table-header-row > span:nth-child(n+3) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collector-size-cell,
|
||||
.collector-hoster-cell,
|
||||
.collector-status-cell,
|
||||
.collector-availability-cell,
|
||||
.collector-added-cell {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collector-table-body {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -326,7 +351,7 @@
|
||||
|
||||
.collector-name-cell.is-file {
|
||||
color: var(--ui-text);
|
||||
padding-left: 36px;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.collector-collapse-button {
|
||||
@@ -404,6 +429,18 @@
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.collector-status-cell.is-online {
|
||||
color: var(--ui-success);
|
||||
}
|
||||
|
||||
.collector-status-cell.is-offline {
|
||||
color: var(--ui-danger);
|
||||
}
|
||||
|
||||
.collector-status-cell.is-partial {
|
||||
color: var(--ui-warning-text);
|
||||
}
|
||||
|
||||
.collector-availability-cell {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
@@ -467,7 +504,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.collector-background-state span {
|
||||
.collector-sidebar-analysis > span {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -32,9 +32,15 @@ export interface CollectorContainerPreparationRequest {
|
||||
}
|
||||
|
||||
export interface CollectorEnrichmentRequest {
|
||||
requestId: string;
|
||||
packages: CollectorPackage[];
|
||||
}
|
||||
|
||||
export interface CollectorEnrichmentProgress {
|
||||
requestId: string;
|
||||
result: CollectorInspectionResult;
|
||||
}
|
||||
|
||||
export interface CollectorInspectionResult {
|
||||
packages: CollectorPackage[];
|
||||
invalidCount: number;
|
||||
@@ -127,7 +133,10 @@ export function validateCollectorEnrichmentRequest(value: unknown): CollectorEnr
|
||||
throw new Error("Linksammler-Anreicherung ist ungültig");
|
||||
}
|
||||
const raw = value as Record<string, unknown>;
|
||||
if (Object.keys(raw).some((key) => key !== "packages")
|
||||
if (Object.keys(raw).some((key) => key !== "requestId" && key !== "packages")
|
||||
|| typeof raw.requestId !== "string"
|
||||
|| raw.requestId.length === 0
|
||||
|| raw.requestId.length > 160
|
||||
|| !Array.isArray(raw.packages)
|
||||
|| raw.packages.length === 0
|
||||
|| raw.packages.length > 2_000
|
||||
@@ -135,7 +144,7 @@ export function validateCollectorEnrichmentRequest(value: unknown): CollectorEnr
|
||||
|| raw.packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > 20_000) {
|
||||
throw new Error("Linksammler-Anreicherung ist ungültig");
|
||||
}
|
||||
return { packages: structuredClone(raw.packages) as CollectorPackage[] };
|
||||
return { requestId: raw.requestId, packages: structuredClone(raw.packages) as CollectorPackage[] };
|
||||
}
|
||||
|
||||
function collectorMarkerValue(value: string): string {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const IPC_CHANNELS = {
|
||||
PREPARE_COLLECTOR_TEXT: "collector:prepare-text",
|
||||
PREPARE_COLLECTOR_CONTAINERS: "collector:prepare-containers",
|
||||
ENRICH_COLLECTOR_PACKAGES: "collector:enrich-packages",
|
||||
COLLECTOR_ENRICHMENT_PROGRESS: "collector:enrichment-progress",
|
||||
GET_START_CONFLICTS: "queue:get-start-conflicts",
|
||||
RESOLVE_START_CONFLICT: "queue:resolve-start-conflict",
|
||||
CLEAR_ALL: "queue:clear-all",
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
import { isRealDebridWebAccountId } from "./real-debrid-accounts";
|
||||
import type {
|
||||
CollectorEnrichmentRequest,
|
||||
CollectorEnrichmentProgress,
|
||||
CollectorInspectionResult,
|
||||
CollectorTextPreparationRequest
|
||||
} from "./collector";
|
||||
@@ -87,6 +88,7 @@ export interface ElectronApi {
|
||||
prepareCollectorText: (request: CollectorTextPreparationRequest) => Promise<CollectorInspectionResult>;
|
||||
prepareCollectorContainers: (filePaths: string[], addedAt: number) => Promise<CollectorInspectionResult>;
|
||||
enrichCollectorPackages: (request: CollectorEnrichmentRequest) => Promise<CollectorInspectionResult>;
|
||||
onCollectorEnrichmentProgress: (callback: (progress: CollectorEnrichmentProgress) => void) => () => void;
|
||||
getPathForDroppedFile: (file: File) => string;
|
||||
getStartConflicts: () => Promise<StartConflictEntry[]>;
|
||||
resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise<StartConflictResolutionResult>;
|
||||
|
||||
Reference in New Issue
Block a user