Daily traffic limits: - Per-provider daily download limit (configurable in GB per provider) - Per Debrid-Link API key daily limit (individual limits per key) - Usage tracking with automatic daily reset at midnight - Provider is skipped when daily limit reached, falls back to next provider - Reset button per provider and per Debrid-Link key in account settings - Hoster routing skips daily-limited providers gracefully Debrid-Link multi-key improvements: - Keys now display with labels (#1, #2...) and masked tokens in account list - Option to show detailed per-key view with individual usage stats - Keys that hit their daily limit are automatically skipped - providerAccountId/providerAccountLabel stored per download item Auto-sort packages by progress: - Active packages automatically sorted to top during downloads - Sorted by completion ratio, then downloaded bytes - Toggle in settings (autoSortPackagesByProgress) UI polish: - Package column headers: flatter, more transparent design - LinkSnappy mode label: "Login" renamed to "Web" - Account list: new toggle for detailed Debrid-Link key display - Account usage stats section with warning styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import type { DownloadItem, DownloadStatus, PackageEntry } from "../shared/types";
|
|
|
|
const ACTIVE_PACKAGE_STATUSES = new Set<DownloadStatus>(["downloading", "validating", "integrity_check", "extracting"]);
|
|
|
|
export function reorderPackageOrderByDrop(order: string[], draggedPackageId: string, targetPackageId: string): string[] {
|
|
const fromIndex = order.indexOf(draggedPackageId);
|
|
const toIndex = order.indexOf(targetPackageId);
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
|
|
return order;
|
|
}
|
|
const next = [...order];
|
|
const [dragged] = next.splice(fromIndex, 1);
|
|
const insertIndex = Math.max(0, Math.min(next.length, toIndex));
|
|
next.splice(insertIndex, 0, dragged);
|
|
return next;
|
|
}
|
|
|
|
export function sortPackageOrderByName(order: string[], packages: Record<string, PackageEntry>, descending: boolean): string[] {
|
|
const sorted = [...order];
|
|
sorted.sort((a, b) => {
|
|
const nameA = (packages[a]?.name ?? "").toLowerCase();
|
|
const nameB = (packages[b]?.name ?? "").toLowerCase();
|
|
const cmp = nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: "base" });
|
|
return descending ? -cmp : cmp;
|
|
});
|
|
return sorted;
|
|
}
|
|
|
|
export function sortPackagesForDisplay(
|
|
packages: PackageEntry[],
|
|
itemsById: Record<string, DownloadItem>,
|
|
running: boolean,
|
|
autoSortPackagesByProgress: boolean
|
|
): PackageEntry[] {
|
|
if (!running || !autoSortPackagesByProgress || packages.length <= 1) {
|
|
return packages;
|
|
}
|
|
|
|
const active: Array<{ pkg: PackageEntry; index: number; completedRatio: number; downloadedBytes: number }> = [];
|
|
const rest: PackageEntry[] = [];
|
|
|
|
packages.forEach((pkg, index) => {
|
|
const items = pkg.itemIds
|
|
.map((id) => itemsById[id])
|
|
.filter((item): item is DownloadItem => Boolean(item));
|
|
const hasActive = items.some((item) => ACTIVE_PACKAGE_STATUSES.has(item.status));
|
|
if (!hasActive) {
|
|
rest.push(pkg);
|
|
return;
|
|
}
|
|
const completedRatio = items.length > 0
|
|
? items.filter((item) => item.status === "completed").length / items.length
|
|
: 0;
|
|
const downloadedBytes = items.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0);
|
|
active.push({ pkg, index, completedRatio, downloadedBytes });
|
|
});
|
|
|
|
if (active.length === 0 || active.length === packages.length) {
|
|
return packages;
|
|
}
|
|
|
|
active.sort((a, b) => {
|
|
if (a.completedRatio !== b.completedRatio) {
|
|
return b.completedRatio - a.completedRatio;
|
|
}
|
|
if (a.downloadedBytes !== b.downloadedBytes) {
|
|
return b.downloadedBytes - a.downloadedBytes;
|
|
}
|
|
return a.index - b.index;
|
|
});
|
|
|
|
return [...active.map((entry) => entry.pkg), ...rest];
|
|
}
|