Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/components/ProgressPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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";
Expand Down Expand Up @@ -77,7 +85,7 @@ export const ProgressModalContent: VFC<{ closeModal?: () => void }> = ({ closeMo
};

return (
<ModalRoot closeModal={closeModal}>
<ModalRoot closeModal={handleClose}>
<style>{getModalCSS()}</style>
<div className="cd-modal-progress">
<img src={mascotUrl} alt="" className="cd-modal-mascot" />
Expand Down
44 changes: 43 additions & 1 deletion src/eventPoller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(<ProgressModalContent />);
progressModalHandle = showModal(
<ProgressModalContent onUserDismiss={handleUserDismissProgressModal} />
);
}
// 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);
}

Expand Down
Loading