mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-16 14:28:21 +00:00
feat(properties): harden standalone properties lifecycle
- synchronize child appearance and hidden-window readiness - serialize native Torrent mutations transactionally without double-encoded rows - preserve native lifecycle markers and fence stale or duplicate actions - remove the obsolete modal surface and ignore implementation_plan.md
This commit is contained in:
+183
-13
@@ -1,5 +1,9 @@
|
||||
import { emitTo } from '@tauri-apps/api/event';
|
||||
import type { DownloadProgressEvent } from './bindings/DownloadProgressEvent';
|
||||
import type { DownloadStatus } from './bindings/DownloadStatus';
|
||||
import type { DownloadItem } from './store/useDownloadStore';
|
||||
import { canPauseDownload } from './utils/downloadActions';
|
||||
import type { DocumentAppearance } from './utils/documentAppearance';
|
||||
import { invokeCommand as invoke } from './ipc';
|
||||
|
||||
export const PROPERTIES_WINDOW_READY = 'properties-window-ready' as const;
|
||||
@@ -9,11 +13,77 @@ export const PROPERTIES_WINDOW_ACTION_RESULT = 'properties-window-action-result'
|
||||
export const PROPERTIES_WINDOW_REMOVED = 'properties-window-removed' as const;
|
||||
export const PROPERTIES_WINDOW_CLOSED = 'properties-window-closed' as const;
|
||||
|
||||
export type PropertiesSnapshot = Omit<DownloadItem, 'password' | 'cookies' | 'headers' | 'username'> & {
|
||||
const PROPERTIES_SNAPSHOT_KEYS = [
|
||||
'id',
|
||||
'url',
|
||||
'fileName',
|
||||
'status',
|
||||
'fraction',
|
||||
'speed',
|
||||
'eta',
|
||||
'size',
|
||||
'downloadedBytes',
|
||||
'totalBytes',
|
||||
'totalIsEstimate',
|
||||
'category',
|
||||
'dateAdded',
|
||||
'resumable',
|
||||
'connections',
|
||||
'speedLimit',
|
||||
'checksum',
|
||||
'destination',
|
||||
'isMedia',
|
||||
'mediaFormatSelector',
|
||||
'mediaQuality',
|
||||
'queueId',
|
||||
'queuePosition',
|
||||
'hasBeenDispatched',
|
||||
'lastError',
|
||||
'lastTry',
|
||||
'isTorrent',
|
||||
'torrentFileIndices',
|
||||
'torrentInfoHash',
|
||||
'torrentSeedTime',
|
||||
'torrentSeedRatio',
|
||||
'torrentSeedRemaining',
|
||||
'torrentUploadedBytes',
|
||||
'torrentSeededSeconds',
|
||||
'torrentRelocationCheckPending',
|
||||
'torrentMoveDestination',
|
||||
'torrentMoveRestoreStatus',
|
||||
'torrentWebSeeds',
|
||||
'torrentUploadLimit',
|
||||
'torrentMaxPeers',
|
||||
'torrentPeerSpeedLimit',
|
||||
'torrentCheckIntegrity',
|
||||
'torrentTrackers',
|
||||
'torrentExcludeTrackers',
|
||||
'torrentTrackerConnectTimeout',
|
||||
'torrentTrackerTimeout',
|
||||
'torrentTrackerInterval',
|
||||
'torrentStopTimeout',
|
||||
'torrentPrioritizePiece',
|
||||
'torrentRemoveUnselectedFile',
|
||||
'torrentEncryptionPolicy',
|
||||
'torrentFileAllocation',
|
||||
'torrentVerifyOnly',
|
||||
'torrentVerifyRestoreStatus',
|
||||
] as const satisfies readonly (keyof DownloadItem)[];
|
||||
|
||||
type SafePropertiesFields = Pick<DownloadItem, (typeof PROPERTIES_SNAPSHOT_KEYS)[number]>;
|
||||
|
||||
export type PropertiesSnapshot = SafePropertiesFields & {
|
||||
appearance: DocumentAppearance;
|
||||
activeConnections?: number;
|
||||
requestedConnections?: number;
|
||||
uploadSpeed?: string;
|
||||
torrentSeeders?: number;
|
||||
moveProgress?: number;
|
||||
hasPassword: boolean;
|
||||
hasCookies: boolean;
|
||||
hasHeaders: boolean;
|
||||
hasUsername: boolean;
|
||||
hasMirrors: boolean;
|
||||
};
|
||||
|
||||
export type SecretPatch =
|
||||
@@ -31,10 +101,39 @@ export type PropertiesPatch = Partial<Omit<DownloadItem, 'password' | 'cookies'
|
||||
export type PropertiesAction =
|
||||
| 'apply-properties'
|
||||
| 'pause-resume'
|
||||
| 'verify-torrent'
|
||||
| 'set-download-limit'
|
||||
| 'set-torrent-upload-limit'
|
||||
| 'set-torrent-peer-options';
|
||||
|
||||
export type PropertiesLifecycleAction = 'pause' | 'resume' | 'start' | 'retry';
|
||||
|
||||
export const getPropertiesLifecycleAction = (
|
||||
status: DownloadStatus,
|
||||
): PropertiesLifecycleAction | null => {
|
||||
if (status === 'ready' || status === 'staged') return 'start';
|
||||
if (canPauseDownload(status)) return 'pause';
|
||||
if (status === 'paused') return 'resume';
|
||||
if (status === 'failed') return 'retry';
|
||||
return null;
|
||||
};
|
||||
|
||||
export const beginExclusivePropertiesAction = (
|
||||
inFlight: Set<string>,
|
||||
key: string,
|
||||
): (() => void) => {
|
||||
if (inFlight.has(key)) {
|
||||
throw new Error('Another Properties action is still in progress');
|
||||
}
|
||||
inFlight.add(key);
|
||||
let released = false;
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
inFlight.delete(key);
|
||||
};
|
||||
};
|
||||
|
||||
export type PropertiesWindowReady = {
|
||||
windowLabel: string;
|
||||
downloadId: string;
|
||||
@@ -63,25 +162,96 @@ export type PropertiesSnapshotEvent = {
|
||||
snapshot: PropertiesSnapshot;
|
||||
};
|
||||
|
||||
const copyWithoutSecrets = (item: DownloadItem): PropertiesSnapshot => {
|
||||
const {
|
||||
password,
|
||||
cookies,
|
||||
headers,
|
||||
username,
|
||||
...safeItem
|
||||
} = item;
|
||||
const copyWithoutSecrets = (
|
||||
item: DownloadItem,
|
||||
appearance: DocumentAppearance,
|
||||
live?: {
|
||||
progress?: DownloadProgressEvent;
|
||||
moveProgress?: number;
|
||||
},
|
||||
): PropertiesSnapshot => {
|
||||
const safeItem = Object.fromEntries(
|
||||
PROPERTIES_SNAPSHOT_KEYS.flatMap(key => (
|
||||
Object.prototype.hasOwnProperty.call(item, key) ? [[key, item[key]]] : []
|
||||
)),
|
||||
) as SafePropertiesFields;
|
||||
return {
|
||||
...safeItem,
|
||||
hasPassword: Boolean(password),
|
||||
hasCookies: Boolean(cookies),
|
||||
hasHeaders: Boolean(headers),
|
||||
hasUsername: Boolean(username),
|
||||
appearance,
|
||||
...(live?.progress ? {
|
||||
fraction: live.progress.fraction,
|
||||
speed: item.status === 'seeding'
|
||||
? live.progress.upload_speed ?? live.progress.speed
|
||||
: live.progress.speed,
|
||||
eta: item.status === 'seeding' ? '-' : live.progress.eta,
|
||||
...(live.progress.size ? { size: live.progress.size } : {}),
|
||||
...(live.progress.downloaded_bytes !== undefined
|
||||
? { downloadedBytes: live.progress.downloaded_bytes }
|
||||
: {}),
|
||||
...(live.progress.total_bytes !== undefined
|
||||
? { totalBytes: live.progress.total_bytes }
|
||||
: {}),
|
||||
...(live.progress.total_is_estimate !== undefined
|
||||
? { totalIsEstimate: live.progress.total_is_estimate }
|
||||
: {}),
|
||||
...(live.progress.active_connections !== undefined
|
||||
? { activeConnections: live.progress.active_connections }
|
||||
: {}),
|
||||
...(live.progress.requested_connections !== undefined
|
||||
? { requestedConnections: live.progress.requested_connections }
|
||||
: {}),
|
||||
...(live.progress.uploaded_bytes !== undefined
|
||||
? { torrentUploadedBytes: live.progress.uploaded_bytes }
|
||||
: {}),
|
||||
...(live.progress.upload_speed !== undefined
|
||||
? { uploadSpeed: live.progress.upload_speed }
|
||||
: {}),
|
||||
...(live.progress.num_seeders !== undefined
|
||||
? { torrentSeeders: live.progress.num_seeders }
|
||||
: {}),
|
||||
...(live.progress.torrent_seeded_seconds !== undefined
|
||||
? { torrentSeededSeconds: live.progress.torrent_seeded_seconds }
|
||||
: {}),
|
||||
} : {}),
|
||||
...(live?.moveProgress !== undefined ? { moveProgress: live.moveProgress } : {}),
|
||||
hasPassword: Boolean(item.password),
|
||||
hasCookies: Boolean(item.cookies),
|
||||
hasHeaders: Boolean(item.headers),
|
||||
hasUsername: Boolean(item.username),
|
||||
hasMirrors: Boolean(item.mirrors),
|
||||
};
|
||||
};
|
||||
|
||||
export const sanitizePropertiesSnapshot = copyWithoutSecrets;
|
||||
|
||||
export const createFrameCoalescer = (
|
||||
callback: (key: string) => void,
|
||||
requestFrame: (callback: FrameRequestCallback) => number,
|
||||
cancelFrame: (handle: number) => void,
|
||||
) => {
|
||||
const pending = new Map<string, number>();
|
||||
return {
|
||||
schedule(key: string) {
|
||||
if (pending.has(key)) return;
|
||||
const handle = requestFrame(() => {
|
||||
pending.delete(key);
|
||||
callback(key);
|
||||
});
|
||||
pending.set(key, handle);
|
||||
},
|
||||
cancel(key: string) {
|
||||
const handle = pending.get(key);
|
||||
if (handle === undefined) return;
|
||||
pending.delete(key);
|
||||
cancelFrame(handle);
|
||||
},
|
||||
cancelAll() {
|
||||
for (const handle of pending.values()) cancelFrame(handle);
|
||||
pending.clear();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const openPropertiesWindow = (downloadId: string): Promise<string> =>
|
||||
invoke('open_download_properties_window', { id: downloadId });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user