import type { DebridProvider, RendererSettings, RendererSettingsUpdate } from "../shared/types"; export type AccountToggleTarget = | { type: "provider"; provider: DebridProvider } | { type: "realdebrid"; accountId: string } | { type: "megadebrid"; provider: "megadebrid-api" | "megadebrid-web"; accountId: string } | { type: "debridlink"; accountId: string }; export type AccountToggleQueueResult = | { status: "applied"; value: T } | { status: "failed"; error: unknown } | { status: "superseded" }; export interface AccountToggleQueue { enqueue(key: string, task: (isCurrent: () => boolean) => Promise): Promise>; } export function resolveAccountToggleIntentEnabled(pendingEnabled: boolean | undefined, eventEnabled: boolean): boolean { return pendingEnabled === undefined ? eventEnabled : !pendingEnabled; } export function createAccountToggleQueue(): AccountToggleQueue { let tail = Promise.resolve(); const versions = new Map(); return { enqueue(key: string, task: (isCurrent: () => boolean) => Promise): Promise> { const version = (versions.get(key) || 0) + 1; versions.set(key, version); const isCurrent = (): boolean => versions.get(key) === version; const execute = async (): Promise> => { if (!isCurrent()) return { status: "superseded" }; try { const value = await task(isCurrent); return isCurrent() ? { status: "applied", value } : { status: "superseded" }; } catch (error) { return isCurrent() ? { status: "failed", error } : { status: "superseded" }; } }; const result = tail.then(execute); tail = result.then(() => undefined); return result; } }; } export type AccountModeFilter = "all" | "api" | "web"; export interface AccountModeOption { modeLabel: string; } export function sortAccountServices(labels: readonly string[]): string[] { return [...new Set(labels)].sort((left, right) => left.localeCompare(right, "de-DE", { sensitivity: "base" })); } export function getAvailableAccountOptions( options: readonly T[], configuredServices: readonly string[] ): T[] { const configured = new Set(configuredServices); return options.filter((option) => option.service === "realdebrid" || option.service === "megadebrid-api" || option.service === "megadebrid-web" || option.service === "debridlink" || !configured.has(option.service)); } export function matchesAccountModeFilter(option: AccountModeOption, filter: AccountModeFilter): boolean { if (filter === "all") { return true; } if (filter === "api") { return option.modeLabel === "API"; } return option.modeLabel.startsWith("Web"); } export function filterAccountDialogOptions(options: readonly T[], query: string, serviceFilter: string): T[] { const normalizedQuery = query.trim().toLocaleLowerCase("de-DE"); return options.filter((option) => { if (serviceFilter !== "all" && option.serviceLabel !== serviceFilter) { return false; } if (!normalizedQuery) { return true; } return [option.title, option.modeLabel, option.pickerDescription] .join(" ") .toLocaleLowerCase("de-DE") .includes(normalizedQuery); }).sort((left, right) => left.serviceLabel.localeCompare(right.serviceLabel, "de-DE", { sensitivity: "base" }) || left.title.localeCompare(right.title, "de-DE", { sensitivity: "base" })); } export function buildConfiguredProviderOrder( currentOrder: readonly DebridProvider[], configuredProviders: readonly DebridProvider[] ): DebridProvider[] { const configured = new Set(configuredProviders); const result = currentOrder.filter((provider, index) => configured.has(provider) && currentOrder.indexOf(provider) === index); const included = new Set(result); for (const provider of configuredProviders) { if (!included.has(provider)) { result.push(provider); included.add(provider); } } return result; } export function pruneAccountRowSelection(selectedRowKey: string | null, existingRowKeys: readonly string[]): string | null { return selectedRowKey && existingRowKeys.includes(selectedRowKey) ? selectedRowKey : null; } export function updateAccountRowSelection(selectedRowKeys: readonly string[], rowKey: string, additive: boolean): string[] { if (!additive) { return [rowKey]; } const next = new Set(selectedRowKeys); if (next.has(rowKey)) { next.delete(rowKey); } else { next.add(rowKey); } return [...next]; } export function pruneAccountRowSelections(selectedRowKeys: readonly string[], existingRowKeys: readonly string[]): string[] { const existing = new Set(existingRowKeys); return [...new Set(selectedRowKeys)].filter((rowKey) => existing.has(rowKey)); } export function resolveVisibleAccountKind(currentKind: T | null, visibleKinds: readonly T[]): T | null { return currentKind && visibleKinds.includes(currentKind) ? currentKind : visibleKinds[0] ?? null; } export function getAccountDialogSelectableOptions( allOptions: readonly T[], availableOptions: readonly T[], mode: "create" | "edit", editService: string | null ): T[] { return mode === "edit" ? allOptions.filter((option) => option.service === editService) : [...availableOptions]; } export function isAccountRowSelectionKey(key: string, originatedOnRow: boolean): boolean { return originatedOnRow && (key === "Enter" || key === " "); } export function resolveAccountUsername(storedUsername: string, checkedEmail?: string): string { return checkedEmail?.trim() || storedUsername.trim() || "—"; } export function resolveAccountStatusState( disabled: boolean, checkedStatus?: { valid: boolean; isPremium: boolean; premiumUntilMs?: number | null }, nowMs: number = Date.now() ): "disabled" | "unchecked" | "invalid" | "free" | "premium" { if (checkedStatus && !checkedStatus.valid) { return "invalid"; } if (disabled) { return "disabled"; } if (!checkedStatus) { return "unchecked"; } if (checkedStatus.isPremium && typeof checkedStatus.premiumUntilMs === "number" && Number.isFinite(checkedStatus.premiumUntilMs) && checkedStatus.premiumUntilMs > 0 && checkedStatus.premiumUntilMs <= nowMs) { return "free"; } return checkedStatus.isPremium ? "premium" : "free"; } export function buildScopedAccountEnabledState( currentDisabledProviders: DebridProvider[], providerIds: DebridProvider[], currentDisabledAccountIds: string[], accountId: string, enabled: boolean ): { disabledProviders: DebridProvider[]; disabledAccountIds: string[] } { const providers = new Set(providerIds); return { disabledProviders: enabled ? currentDisabledProviders.filter((provider) => !providers.has(provider)) : [...currentDisabledProviders], disabledAccountIds: enabled ? currentDisabledAccountIds.filter((id) => id !== accountId) : [...new Set([...currentDisabledAccountIds, accountId])] }; } export function buildAccountTogglePatch( settings: RendererSettings, target: AccountToggleTarget, enabled: boolean ): RendererSettingsUpdate { if (target.type === "provider") { return { disabledProviders: enabled ? settings.disabledProviders.filter((provider) => provider !== target.provider) : [...new Set([...settings.disabledProviders, target.provider])] }; } if (target.type === "realdebrid") { const next = buildScopedAccountEnabledState( settings.disabledProviders, ["realdebrid"], settings.realDebridDisabledAccountIds, target.accountId, enabled ); return { disabledProviders: next.disabledProviders, realDebridDisabledAccountIds: next.disabledAccountIds }; } if (target.type === "debridlink") { const next = buildScopedAccountEnabledState( settings.disabledProviders, ["debridlink"], settings.debridLinkDisabledKeyIds, target.accountId, enabled ); return { disabledProviders: next.disabledProviders, debridLinkDisabledKeyIds: next.disabledAccountIds }; } const web = target.provider === "megadebrid-web"; const currentDisabledIds = web ? settings.megaDebridWebDisabledAccountIds : settings.megaDebridApiDisabledAccountIds; const next = buildScopedAccountEnabledState( settings.disabledProviders, ["megadebrid", target.provider], currentDisabledIds, target.accountId, enabled ); const apiDisabledIds = web ? settings.megaDebridApiDisabledAccountIds : next.disabledAccountIds; const webDisabledIds = web ? next.disabledAccountIds : settings.megaDebridWebDisabledAccountIds; return { disabledProviders: next.disabledProviders, megaDebridApiEnabled: !web && enabled ? true : settings.megaDebridApiEnabled, megaDebridWebEnabled: web && enabled ? true : settings.megaDebridWebEnabled, megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])], megaDebridApiDisabledAccountIds: apiDisabledIds, megaDebridWebDisabledAccountIds: webDisabledIds }; } export interface AccountToggleIntent { key: string; target: AccountToggleTarget; enabled: boolean; check?: () => Promise<{ valid: boolean; message?: string }>; } export interface AccountToggleIntentDependencies { getSettings: () => RendererSettings; persist: (patch: RendererSettingsUpdate) => Promise; } export function enqueueAccountToggleIntent( queue: AccountToggleQueue, intent: AccountToggleIntent, dependencies: AccountToggleIntentDependencies ): Promise> { return queue.enqueue(intent.key, async (isCurrent) => { if (intent.check) { const status = await intent.check(); if (!isCurrent()) return dependencies.getSettings(); if (!status.valid) throw new Error(status.message || "Accountprüfung fehlgeschlagen"); } if (!isCurrent()) return dependencies.getSettings(); const patch = buildAccountTogglePatch(dependencies.getSettings(), intent.target, intent.enabled); return dependencies.persist(patch); }); } export function mergeAccountToggleSettings(current: T, persisted: RendererSettings): T { return { ...current, disabledProviders: [...persisted.disabledProviders], realDebridDisabledAccountIds: [...persisted.realDebridDisabledAccountIds], debridLinkDisabledKeyIds: [...persisted.debridLinkDisabledKeyIds], megaDebridApiEnabled: persisted.megaDebridApiEnabled, megaDebridWebEnabled: persisted.megaDebridWebEnabled, megaDebridDisabledAccountIds: [...persisted.megaDebridDisabledAccountIds], megaDebridApiDisabledAccountIds: [...persisted.megaDebridApiDisabledAccountIds], megaDebridWebDisabledAccountIds: [...persisted.megaDebridWebDisabledAccountIds] }; }