mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-22 17:06:40 +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:
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ import {
|
||||
PROPERTIES_WINDOW_ACTION_RESULT,
|
||||
PROPERTIES_WINDOW_REMOVED,
|
||||
PROPERTIES_WINDOW_SNAPSHOT,
|
||||
getPropertiesLifecycleAction,
|
||||
sendPropertiesActionRequest,
|
||||
sendPropertiesReady,
|
||||
type PropertiesAction,
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
type PropertiesSnapshotEvent,
|
||||
} from '../propertiesBridge';
|
||||
import { formatDownloadBytes, formatTorrentRatio } from '../utils/downloadProgress';
|
||||
import { changeAppLocale } from '../i18n';
|
||||
import { synchronizeDocumentAppearance } from '../utils/documentAppearance';
|
||||
|
||||
type PropertiesTab = 'overview' | 'files' | 'trackers' | 'peers' | 'options' | 'transfer' | 'advanced';
|
||||
|
||||
@@ -73,7 +76,11 @@ export const PropertiesWindowApp = () => {
|
||||
const closeAfterSaveRef = useRef(false);
|
||||
const switchAfterSaveRef = useRef<PropertiesTab | null>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
const pendingActionRef = useRef<PropertiesAction | null>(null);
|
||||
const latestSnapshotRevisionRef = useRef(0);
|
||||
const appearanceCleanupRef = useRef<(() => void) | null>(null);
|
||||
const hasRevealedWindowRef = useRef(false);
|
||||
const revealInFlightRef = useRef(false);
|
||||
const diagnosticsInFlightRef = useRef(new Set<string>());
|
||||
const snapshotRef = useRef(snapshot);
|
||||
const activeTabRef = useRef(activeTab);
|
||||
@@ -140,6 +147,7 @@ export const PropertiesWindowApp = () => {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let readyRetryTimer: number | undefined;
|
||||
let unlistenSnapshot: UnlistenFn | undefined;
|
||||
let unlistenResult: UnlistenFn | undefined;
|
||||
let unlistenRemoved: UnlistenFn | undefined;
|
||||
@@ -148,18 +156,40 @@ export const PropertiesWindowApp = () => {
|
||||
const id = await invoke('get_properties_window_download_id');
|
||||
if (cancelled) return;
|
||||
setDownloadId(id);
|
||||
unlistenSnapshot = await listen<PropertiesSnapshotEvent>(PROPERTIES_WINDOW_SNAPSHOT, event => {
|
||||
unlistenSnapshot = await listen<PropertiesSnapshotEvent>(PROPERTIES_WINDOW_SNAPSHOT, async event => {
|
||||
if (event.payload.windowLabel !== windowLabel || event.payload.downloadId !== id) return;
|
||||
if (event.payload.revision <= latestSnapshotRevisionRef.current) return;
|
||||
latestSnapshotRevisionRef.current = event.payload.revision;
|
||||
await changeAppLocale(event.payload.snapshot.appearance.locale);
|
||||
if (event.payload.revision !== latestSnapshotRevisionRef.current) return;
|
||||
appearanceCleanupRef.current?.();
|
||||
appearanceCleanupRef.current = synchronizeDocumentAppearance(
|
||||
window,
|
||||
event.payload.snapshot.appearance,
|
||||
);
|
||||
setSnapshot(event.payload.snapshot);
|
||||
if (draftTabRef.current === null) hydrateDraft(event.payload.snapshot);
|
||||
void currentWindow.setTitle(safeTitle(event.payload.snapshot.fileName)).catch(() => undefined);
|
||||
if (!hasRevealedWindowRef.current && !revealInFlightRef.current) {
|
||||
revealInFlightRef.current = true;
|
||||
try {
|
||||
await invoke('properties_window_reveal');
|
||||
hasRevealedWindowRef.current = true;
|
||||
if (readyRetryTimer !== undefined) {
|
||||
window.clearInterval(readyRetryTimer);
|
||||
readyRetryTimer = undefined;
|
||||
}
|
||||
} finally {
|
||||
revealInFlightRef.current = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
unlistenResult = await listen<PropertiesActionResult>(PROPERTIES_WINDOW_ACTION_RESULT, event => {
|
||||
if (event.payload.windowLabel !== windowLabel || event.payload.downloadId !== id) return;
|
||||
if (event.payload.requestId !== requestIdRef.current) return;
|
||||
setIsSaving(false);
|
||||
const completedAction = pendingActionRef.current;
|
||||
pendingActionRef.current = null;
|
||||
if (!event.payload.ok) setErrorMessage(event.payload.error ?? 'The action failed');
|
||||
else {
|
||||
const nextTab = switchAfterSaveRef.current;
|
||||
@@ -167,7 +197,7 @@ export const PropertiesWindowApp = () => {
|
||||
switchAfterSaveRef.current = null;
|
||||
closeAfterSaveRef.current = false;
|
||||
setErrorMessage('');
|
||||
setNotice(t($ => $.properties.saved));
|
||||
setNotice(completedAction === 'apply-properties' ? t($ => $.properties.saved) : '');
|
||||
draftTabRef.current = null;
|
||||
setDraftTab(null);
|
||||
if (nextTab) {
|
||||
@@ -182,11 +212,24 @@ export const PropertiesWindowApp = () => {
|
||||
});
|
||||
unlistenRemoved = await listen<{ windowLabel: string; downloadId: string }>(PROPERTIES_WINDOW_REMOVED, event => {
|
||||
if (event.payload.windowLabel === windowLabel && event.payload.downloadId === id) {
|
||||
if (readyRetryTimer !== undefined) {
|
||||
window.clearInterval(readyRetryTimer);
|
||||
readyRetryTimer = undefined;
|
||||
}
|
||||
setSnapshot(null);
|
||||
setNotice(t($ => $.downloadTable.noDownloads));
|
||||
}
|
||||
});
|
||||
await sendPropertiesReady();
|
||||
if (cancelled) return;
|
||||
// Tauri event listeners are registered asynchronously. If the main
|
||||
// bridge was still installing its listener, the first ready event can
|
||||
// legitimately be missed; retry until the first snapshot confirms
|
||||
// the handshake rather than leaving a permanently hidden window.
|
||||
readyRetryTimer = window.setInterval(() => {
|
||||
if (cancelled || hasRevealedWindowRef.current) return;
|
||||
void sendPropertiesReady().catch(() => undefined);
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
if (!cancelled) setErrorMessage(errorText(error));
|
||||
}
|
||||
@@ -194,9 +237,12 @@ export const PropertiesWindowApp = () => {
|
||||
void start();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (readyRetryTimer !== undefined) window.clearInterval(readyRetryTimer);
|
||||
unlistenSnapshot?.();
|
||||
unlistenResult?.();
|
||||
unlistenRemoved?.();
|
||||
appearanceCleanupRef.current?.();
|
||||
appearanceCleanupRef.current = null;
|
||||
};
|
||||
}, [currentWindow, hydrateDraft, t, windowLabel]);
|
||||
|
||||
@@ -228,8 +274,10 @@ export const PropertiesWindowApp = () => {
|
||||
payload?: PropertiesActionRequest['payload'],
|
||||
) => {
|
||||
if (!downloadId) return;
|
||||
if (pendingActionRef.current !== null) return;
|
||||
const requestId = ++requestIdRef.current;
|
||||
setIsSaving(action === 'apply-properties');
|
||||
pendingActionRef.current = action;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await sendPropertiesActionRequest({
|
||||
windowLabel,
|
||||
@@ -240,6 +288,7 @@ export const PropertiesWindowApp = () => {
|
||||
});
|
||||
} catch (error) {
|
||||
setIsSaving(false);
|
||||
pendingActionRef.current = null;
|
||||
closeAfterSaveRef.current = false;
|
||||
switchAfterSaveRef.current = null;
|
||||
setErrorMessage(errorText(error));
|
||||
@@ -328,8 +377,7 @@ export const PropertiesWindowApp = () => {
|
||||
setNotice(t($ => $.properties.torrentMoveCompleted));
|
||||
}
|
||||
} else {
|
||||
await invoke('verify_torrent_data', { id: downloadId });
|
||||
setNotice(t($ => $.properties.torrentVerifyIntegrity));
|
||||
await requestAction('verify-torrent');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(errorText(error));
|
||||
@@ -344,6 +392,7 @@ export const PropertiesWindowApp = () => {
|
||||
}
|
||||
|
||||
const progress = Math.max(0, Math.min(1, snapshot.fraction ?? 0));
|
||||
const lifecycleAction = getPropertiesLifecycleAction(snapshot.status);
|
||||
const total = snapshot.size || (snapshot.totalBytes === undefined
|
||||
? t($ => $.addDownloads.unknownSize)
|
||||
: `${snapshot.totalIsEstimate ? '~' : ''}${formatDownloadBytes(snapshot.totalBytes)}`);
|
||||
@@ -369,14 +418,32 @@ export const PropertiesWindowApp = () => {
|
||||
<p className="mt-1 text-xs text-text-muted" role="status">{statusLabel} · {Math.round(progress * 100)}% · {total}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2" aria-label={t($ => $.actions.continue)}>
|
||||
<button type="button" className="app-button px-3 text-xs" onClick={() => void requestAction('pause-resume')}>
|
||||
{['paused', 'ready', 'staged', 'completed', 'failed'].includes(snapshot.status) ? <Play size={14} /> : <Pause size={14} />}
|
||||
{['paused', 'ready', 'staged', 'completed', 'failed'].includes(snapshot.status) ? t($ => $.downloads.actions.resume) : t($ => $.downloads.actions.pause)}
|
||||
</button>
|
||||
{lifecycleAction && <button
|
||||
type="button"
|
||||
className="app-button px-3 text-xs"
|
||||
disabled={isSaving}
|
||||
onClick={() => {
|
||||
if (lifecycleAction === 'pause'
|
||||
&& snapshot.resumable === false
|
||||
&& !window.confirm(t($ => $.downloadTable.nonResumableOne))) {
|
||||
return;
|
||||
}
|
||||
void requestAction('pause-resume');
|
||||
}}
|
||||
>
|
||||
{lifecycleAction === 'pause' ? <Pause size={14} /> : <Play size={14} />}
|
||||
{lifecycleAction === 'pause'
|
||||
? t($ => $.downloads.actions.pause)
|
||||
: lifecycleAction === 'resume'
|
||||
? t($ => $.downloads.actions.resume)
|
||||
: lifecycleAction === 'retry'
|
||||
? t($ => $.downloads.actions.retry)
|
||||
: t($ => $.downloads.actions.start)}
|
||||
</button>}
|
||||
{isTorrent && <>
|
||||
<button type="button" className="app-button px-3 text-xs" onClick={() => void performTorrentAction('magnet')}><Copy size={14} />{t($ => $.properties.torrentCopyMagnet)}</button>
|
||||
<button type="button" className="app-button px-3 text-xs" onClick={() => void performTorrentAction('export')}><FileDown size={14} />{t($ => $.properties.torrentExportMetadata)}</button>
|
||||
<button type="button" className="app-button px-3 text-xs" onClick={() => void performTorrentAction('move')}><FolderOpen size={14} />{t($ => $.properties.torrentMove)}</button>
|
||||
<button type="button" className="app-button px-3 text-xs" disabled={isSaving} onClick={() => void performTorrentAction('magnet')}><Copy size={14} />{t($ => $.properties.torrentCopyMagnet)}</button>
|
||||
<button type="button" className="app-button px-3 text-xs" disabled={isSaving} onClick={() => void performTorrentAction('export')}><FileDown size={14} />{t($ => $.properties.torrentExportMetadata)}</button>
|
||||
<button type="button" className="app-button px-3 text-xs" disabled={isSaving} onClick={() => void performTorrentAction('move')}><FolderOpen size={14} />{t($ => $.properties.torrentMove)}</button>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,6 +454,7 @@ export const PropertiesWindowApp = () => {
|
||||
<span>{formatDownloadBytes(snapshot.downloadedBytes ?? 0)} / {total}</span>
|
||||
<span>{snapshot.speed || '—'}</span>
|
||||
<span>{snapshot.eta || '—'}</span>
|
||||
<span>{snapshot.activeConnections ?? '—'} / {snapshot.requestedConnections ?? snapshot.connections ?? '—'} {t($ => $.properties.connections)}</span>
|
||||
{isTorrent && <span>{formatTorrentRatio(snapshot.torrentUploadedBytes ?? 0, snapshot.downloadedBytes ?? 0, 'en-US')}</span>}
|
||||
</div>
|
||||
</header>
|
||||
@@ -432,7 +500,7 @@ export const PropertiesWindowApp = () => {
|
||||
<span className="text-text-muted">{t($ => $.properties.torrentDetailsPieces)}</span><span>{details.pieceCount} × {formatDownloadBytes(details.pieceLength)}</span>
|
||||
<span className="text-text-muted">{t($ => $.properties.torrentDetailsPrivate)}</span><span>{details.private ? t($ => $.properties.torrentDetailsPrivateYes) : t($ => $.properties.torrentDetailsPrivateNo)}</span>
|
||||
</div>}
|
||||
{isTorrent && <div className="flex flex-wrap gap-2"><button type="button" className="app-button px-3 text-xs" onClick={() => void performTorrentAction('verify')}><RefreshCw size={14} />{t($ => $.properties.torrentVerifyNow)}</button></div>}
|
||||
{isTorrent && <div className="flex flex-wrap gap-2"><button type="button" className="app-button px-3 text-xs" disabled={isSaving || !['paused', 'completed', 'failed'].includes(snapshot.status)} onClick={() => void performTorrentAction('verify')}><RefreshCw size={14} />{t($ => $.properties.torrentVerifyNow)}</button></div>}
|
||||
</div>}
|
||||
|
||||
{activeTab === 'files' && isTorrent && <div className="space-y-3">
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { useDownloadStore } from '../store/useDownloadStore';
|
||||
import type { DownloadItem } from '../store/useDownloadStore';
|
||||
import { getPauseResumeAction } from '../utils/downloadActions';
|
||||
import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadProgressStore';
|
||||
import {
|
||||
isValidTorrentExcludeTrackerList,
|
||||
isValidTorrentTrackerList,
|
||||
@@ -13,6 +14,9 @@ import {
|
||||
PROPERTIES_WINDOW_CLOSED,
|
||||
PROPERTIES_WINDOW_READY,
|
||||
applySecretPatch,
|
||||
beginExclusivePropertiesAction,
|
||||
createFrameCoalescer,
|
||||
getPropertiesLifecycleAction,
|
||||
sanitizePropertiesSnapshot,
|
||||
sendPropertiesActionResult,
|
||||
sendPropertiesRemoved,
|
||||
@@ -22,6 +26,7 @@ import {
|
||||
type PropertiesWindowReady,
|
||||
} from '../propertiesBridge';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import i18n, { resolveAppLocale } from '../i18n';
|
||||
|
||||
const errorText = (error: unknown) => error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -105,21 +110,41 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
useEffect(() => {
|
||||
const windows = new Map<string, string>();
|
||||
const snapshotRevisions = new Map<string, number>();
|
||||
const actionsInFlight = new Set<string>();
|
||||
let disposed = false;
|
||||
let unlistenReady: UnlistenFn | undefined;
|
||||
let unlistenAction: UnlistenFn | undefined;
|
||||
let unlistenClosed: UnlistenFn | undefined;
|
||||
const snapshotCoalescer = createFrameCoalescer(
|
||||
windowLabel => {
|
||||
const downloadId = windows.get(windowLabel);
|
||||
if (downloadId) void sendFor(windowLabel, downloadId).catch(() => undefined);
|
||||
},
|
||||
callback => window.requestAnimationFrame(callback),
|
||||
handle => window.cancelAnimationFrame(handle),
|
||||
);
|
||||
|
||||
const sendFor = async (windowLabel: string, downloadId: string) => {
|
||||
const item = useDownloadStore.getState().downloads.find(download => download.id === downloadId);
|
||||
if (!item || disposed) return false;
|
||||
const settings = useSettingsStore.getState();
|
||||
const progress = useDownloadProgressStore.getState();
|
||||
const revision = (snapshotRevisions.get(windowLabel) ?? 0) + 1;
|
||||
snapshotRevisions.set(windowLabel, revision);
|
||||
await sendPropertiesSnapshot(windowLabel, {
|
||||
windowLabel,
|
||||
downloadId,
|
||||
revision,
|
||||
snapshot: sanitizePropertiesSnapshot(item),
|
||||
snapshot: sanitizePropertiesSnapshot(item, {
|
||||
theme: settings.theme,
|
||||
fontFamily: settings.fontFamily,
|
||||
appFontSize: settings.appFontSize,
|
||||
listRowDensity: settings.listRowDensity,
|
||||
locale: resolveAppLocale(i18n.language),
|
||||
}, {
|
||||
progress: progress.progressMap[downloadId],
|
||||
moveProgress: progress.moveProgressMap[downloadId],
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
};
|
||||
@@ -144,11 +169,14 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
const handleAction = async (request: PropertiesActionRequest) => {
|
||||
let ok = false;
|
||||
let error: string | undefined;
|
||||
const actionKey = `${request.windowLabel}:${request.downloadId}`;
|
||||
let releaseAction: (() => void) | undefined;
|
||||
try {
|
||||
await invoke('validate_properties_window_request', request);
|
||||
if (windows.get(request.windowLabel) !== request.downloadId) {
|
||||
throw new Error('Properties window is no longer registered');
|
||||
}
|
||||
releaseAction = beginExclusivePropertiesAction(actionsInFlight, actionKey);
|
||||
const store = useDownloadStore.getState();
|
||||
const item = store.downloads.find(download => download.id === request.downloadId);
|
||||
if (!item) throw new Error('Download no longer exists');
|
||||
@@ -172,10 +200,57 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
await store.applyProperties(request.downloadId, safePatch);
|
||||
break;
|
||||
}
|
||||
case 'pause-resume':
|
||||
if (getPauseResumeAction(item.status) === 'pause') await store.pauseDownload(request.downloadId);
|
||||
else await store.resumeDownload(request.downloadId);
|
||||
case 'pause-resume': {
|
||||
const lifecycleAction = getPropertiesLifecycleAction(item.status);
|
||||
if (!lifecycleAction) {
|
||||
throw new Error('This download has no available lifecycle action');
|
||||
}
|
||||
if (lifecycleAction === 'pause') {
|
||||
await store.pauseDownload(request.downloadId);
|
||||
const current = useDownloadStore.getState().downloads.find(download => download.id === request.downloadId);
|
||||
if (!current) throw new Error('Download was removed while pausing');
|
||||
if (!['paused', 'completed', 'failed'].includes(current.status)) {
|
||||
throw new Error('The download did not reach a paused or terminal state');
|
||||
}
|
||||
} else {
|
||||
const resumed = await store.resumeDownload(request.downloadId);
|
||||
if (!resumed) {
|
||||
throw new Error(i18n.t($ => $.downloadTable.backendRejectedStart));
|
||||
}
|
||||
const current = useDownloadStore.getState().downloads.find(download => download.id === request.downloadId);
|
||||
if (!current) throw new Error('Download was removed while starting');
|
||||
// A fast completion is a valid outcome of a successful resume;
|
||||
// only a status that proves the request never left its
|
||||
// pre-action state is a rejected start. Preserve failed as an
|
||||
// error so a real backend failure is not reported as success.
|
||||
if (['paused', 'ready', 'staged', 'failed'].includes(current.status)) {
|
||||
throw new Error(i18n.t($ => $.downloadTable.backendRejectedStart));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'verify-torrent': {
|
||||
if (item.isTorrent !== true
|
||||
|| !['paused', 'completed', 'failed'].includes(item.status)) {
|
||||
throw new Error('Pause the Torrent before verifying its data');
|
||||
}
|
||||
const previousVerifyOnly = item.torrentVerifyOnly;
|
||||
const previousRestoreStatus = item.torrentVerifyRestoreStatus;
|
||||
store.updateDownload(request.downloadId, {
|
||||
torrentVerifyOnly: true,
|
||||
torrentVerifyRestoreStatus: item.status,
|
||||
});
|
||||
try {
|
||||
await invoke('verify_torrent_data', { id: request.downloadId });
|
||||
} catch (verifyError) {
|
||||
useDownloadStore.getState().updateDownload(request.downloadId, {
|
||||
torrentVerifyOnly: previousVerifyOnly,
|
||||
torrentVerifyRestoreStatus: previousRestoreStatus,
|
||||
});
|
||||
throw verifyError;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set-download-limit':
|
||||
await store.setDownloadSpeedLimit(request.downloadId, request.payload && 'limit' in request.payload ? request.payload.limit : null);
|
||||
break;
|
||||
@@ -190,9 +265,21 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
default:
|
||||
throw new Error('Invalid Properties action');
|
||||
}
|
||||
if (!useDownloadStore.getState().downloads.some(download => download.id === request.downloadId)) {
|
||||
throw new Error('Download was removed while applying the action');
|
||||
}
|
||||
ok = true;
|
||||
} catch (caught) {
|
||||
error = errorText(caught);
|
||||
} finally {
|
||||
releaseAction?.();
|
||||
}
|
||||
if (ok) {
|
||||
try {
|
||||
await sendFor(request.windowLabel, request.downloadId);
|
||||
} catch {
|
||||
// Snapshot delivery is best effort across a close/reopen race.
|
||||
}
|
||||
}
|
||||
try {
|
||||
await sendPropertiesActionResult(request.windowLabel, {
|
||||
@@ -206,20 +293,16 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
// The window may have closed between the request and its result.
|
||||
return;
|
||||
}
|
||||
if (ok) {
|
||||
try {
|
||||
await sendFor(request.windowLabel, request.downloadId);
|
||||
} catch {
|
||||
// Snapshot delivery is best effort across a close/reopen race.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void listen<PropertiesWindowReady>(PROPERTIES_WINDOW_READY, event => void handleReady(event.payload)).then(value => { unlistenReady = value; });
|
||||
void listen<PropertiesActionRequest>(PROPERTIES_WINDOW_ACTION_REQUEST, event => void handleAction(event.payload)).then(value => { unlistenAction = value; });
|
||||
void listen<string>(PROPERTIES_WINDOW_CLOSED, event => {
|
||||
const downloadId = windows.get(event.payload);
|
||||
windows.delete(event.payload);
|
||||
snapshotRevisions.delete(event.payload);
|
||||
snapshotCoalescer.cancel(event.payload);
|
||||
if (downloadId) actionsInFlight.delete(`${event.payload}:${downloadId}`);
|
||||
}).then(value => { unlistenClosed = value; });
|
||||
|
||||
const unsubscribeStore = useDownloadStore.subscribe((state, previous) => {
|
||||
@@ -227,19 +310,50 @@ export const PropertiesWindowBridgeHost = () => {
|
||||
const next = state.downloads.find(download => download.id === downloadId);
|
||||
const before = previous.downloads.find(download => download.id === downloadId);
|
||||
if (!next) {
|
||||
snapshotCoalescer.cancel(windowLabel);
|
||||
void sendPropertiesRemoved(windowLabel, downloadId).catch(() => undefined);
|
||||
windows.delete(windowLabel);
|
||||
snapshotRevisions.delete(windowLabel);
|
||||
void invoke('properties_window_registry_remove_for_download', { id: downloadId }).catch(() => undefined);
|
||||
} else if (next !== before) {
|
||||
void sendFor(windowLabel, downloadId).catch(() => undefined);
|
||||
snapshotCoalescer.schedule(windowLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
const unsubscribeProgress = useDownloadProgressStore.subscribe((state, previous) => {
|
||||
for (const [windowLabel, downloadId] of windows) {
|
||||
if (state.progressMap[downloadId] !== previous.progressMap[downloadId]
|
||||
|| state.moveProgressMap[downloadId] !== previous.moveProgressMap[downloadId]) {
|
||||
snapshotCoalescer.schedule(windowLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
const unsubscribeSettings = useSettingsStore.subscribe((state, previous) => {
|
||||
if (state.theme === previous.theme
|
||||
&& state.fontFamily === previous.fontFamily
|
||||
&& state.appFontSize === previous.appFontSize
|
||||
&& state.listRowDensity === previous.listRowDensity
|
||||
&& state.language === previous.language) {
|
||||
return;
|
||||
}
|
||||
for (const windowLabel of windows.keys()) {
|
||||
snapshotCoalescer.schedule(windowLabel);
|
||||
}
|
||||
});
|
||||
const handleLanguageChanged = () => {
|
||||
for (const windowLabel of windows.keys()) {
|
||||
snapshotCoalescer.schedule(windowLabel);
|
||||
}
|
||||
};
|
||||
i18n.on('languageChanged', handleLanguageChanged);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
snapshotCoalescer.cancelAll();
|
||||
unsubscribeStore();
|
||||
unsubscribeProgress();
|
||||
unsubscribeSettings();
|
||||
i18n.off('languageChanged', handleLanguageChanged);
|
||||
unlistenReady?.();
|
||||
unlistenAction?.();
|
||||
unlistenClosed?.();
|
||||
|
||||
Reference in New Issue
Block a user