From 3b0156f1c05e4b47fc3c96037f0eb841908aeb23 Mon Sep 17 00:00:00 2001 From: Matias Galarza Date: Mon, 20 Apr 2026 09:17:09 -0300 Subject: [PATCH] feat: persist progress visibility after modal dismissal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The progress modal is already shown automatically when a transfer starts, but once the user dismissed it there was no way to bring it back — you had to wait for the transfer to finish. Fixes #230. - ProgressModalContent now accepts an `onUserDismiss` callback so the poller can distinguish between a manual dismiss and a programmatic Close() call. - When the user dismisses the modal while a transfer is still active, emit a long-lived recovery toast (duration 10s popup, 24h tray expiration). Tapping the toast reopens the modal via showModal(). - The toast auto-dismisses when the modal is reopened, when the operation completes/errors, or when the programmatic close fires. - A `closingModalProgrammatically` guard prevents our own Close() calls from triggering the recovery toast. --- src/components/ProgressPanel.tsx | 12 +++++++-- src/eventPoller.tsx | 44 +++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/components/ProgressPanel.tsx b/src/components/ProgressPanel.tsx index e5ba1e6..9a8a0e9 100644 --- a/src/components/ProgressPanel.tsx +++ b/src/components/ProgressPanel.tsx @@ -37,7 +37,10 @@ export const progressState = { // ── Progress modal content (rendered inside showModal) ───────────────────── -export const ProgressModalContent: VFC<{ closeModal?: () => void }> = ({ closeModal }) => { +export const ProgressModalContent: VFC<{ + closeModal?: () => void; + onUserDismiss?: () => void; +}> = ({ closeModal, onUserDismiss }) => { const [, rerender] = useState(0); const [cancelling, setCancelling] = useState(false); @@ -48,6 +51,11 @@ export const ProgressModalContent: VFC<{ closeModal?: () => void }> = ({ closeMo const { operation, progress } = progressState; if (!operation) return null; + const handleClose = () => { + onUserDismiss?.(); + closeModal?.(); + }; + const isInstalling = operation.type === "install"; const isComplete = operation.status === "complete"; const isError = operation.status === "error"; @@ -77,7 +85,7 @@ export const ProgressModalContent: VFC<{ closeModal?: () => void }> = ({ closeMo }; return ( - +
diff --git a/src/eventPoller.tsx b/src/eventPoller.tsx index 2330c06..4d79aa9 100644 --- a/src/eventPoller.tsx +++ b/src/eventPoller.tsx @@ -5,6 +5,7 @@ import { showModal } from "@decky/ui"; import { call, toaster } from "@decky/api"; +import type { ToastNotification } from "@decky/api"; import type { ShortcutConfig } from "./hooks/useAgent"; import { ProgressModalContent, progressState } from "./components/ProgressPanel"; @@ -199,17 +200,58 @@ async function handleUpdateArtwork(event: UpdateArtworkEvent) { let progressModalHandle: { Close: () => void } | null = null; let pairingModalHandle: { Close: () => void } | null = null; +let recoveryToastHandle: ToastNotification | null = null; +let closingModalProgrammatically = false; + +function dismissRecoveryToast() { + if (recoveryToastHandle) { + recoveryToastHandle.dismiss(); + recoveryToastHandle = null; + } +} + +function handleUserDismissProgressModal() { + // Ignore callbacks triggered by our own Close() calls. + if (closingModalProgrammatically) return; + + progressModalHandle = null; + + const op = progressState.operation; + // Only surface the recovery toast while the transfer is still running. + if (!op || op.status === "complete" || op.status === "error") return; + + dismissRecoveryToast(); + recoveryToastHandle = toaster.toast({ + title: op.type === "install" ? "Transfer in progress" : "Removal in progress", + body: `${op.gameName} — tap to show progress`, + logo: toastLogo, + duration: 10_000, + expiration: 24 * 60 * 60 * 1000, + showNewIndicator: true, + onClick: () => { + dismissRecoveryToast(); + showProgressModal(); + }, + }); +} function showProgressModal() { if (!progressModalHandle) { - progressModalHandle = showModal(); + progressModalHandle = showModal( + + ); } + // Modal is visible again — no need for the recovery toast. + dismissRecoveryToast(); } function closeProgressModal(delay = 3000) { setTimeout(() => { + closingModalProgrammatically = true; progressModalHandle?.Close(); progressModalHandle = null; + closingModalProgrammatically = false; + dismissRecoveryToast(); }, delay); }