From eaf38e839b839f125b53dcc33be349b0babc645b Mon Sep 17 00:00:00 2001 From: Henrique Velloso Date: Thu, 5 Feb 2026 20:55:05 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20handle=20terminal=20install=20ops=20in?= =?UTF-8?q?=20install=20button=20=20-=20Show=20uninstall=20(delete)=20acti?= =?UTF-8?q?ons=20immediately=20after=20install=20by=20treating=20Completed?= =?UTF-8?q?=20as=20not=20busy=20on=20the=20detail=20screen.=20-=20Ignore?= =?UTF-8?q?=20stale=20Completed=20when=20the=20app=20is=20no=20longer=20in?= =?UTF-8?q?stalled=20so=20the=20primary=20CTA=20returns=20to=20Install.=20?= =?UTF-8?q?-=20Allow=20install=20=E2=86=92=20uninstall=20=E2=86=92=20insta?= =?UTF-8?q?ll=20again=20by=20clearing=20stale=20terminal=20ops=20(Complete?= =?UTF-8?q?d/OperationFailed)=20in=20PackageManager.startDownload()=20(kee?= =?UTF-8?q?ps=20batch=20progress=20UX=20intact).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../package_manager/package_manager.dart | 13 ++++++++++++- lib/widgets/install_button.dart | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index f01fc21..025f495 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -330,7 +330,18 @@ abstract class PackageManager extends StateNotifier { String? displayName, }) async { await _ensureDownloaderReady(); - if (hasOperation(appId)) return false; + final existing = getOperation(appId); + if (existing != null) { + // We keep terminal states (Completed/Failed) in the operations map briefly so + // batch progress UI can derive totals. However, starting a new download for the + // same app should not be blocked by a stale terminal op (e.g. install -> uninstall + // -> install again). Only in-flight operations should block. + if (existing is Completed || existing is OperationFailed) { + clearOperation(appId); + } else { + return false; + } + } final downloadUrl = target.urls.firstOrNull; if (downloadUrl == null || downloadUrl.isEmpty) { diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 8d2e3bd..0ba2814 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -31,6 +31,12 @@ class InstallButton extends ConsumerWidget { // Derive what to display final isInstalled = installedPkg != null; + // Note: PackageManager keeps terminal operations (e.g. Completed) in the + // operations map for a while so batch UX (e.g. "All done") can derive counts. + // For the detail screen action row, Completed should be treated as "not busy" + // so the user can immediately Open/Delete after install. + final canShowActionButtons = + isInstalled && (operation == null || operation is Completed); final hasUpdate = app.hasUpdate; final hasDowngrade = app.hasDowngrade; final hasRelease = release != null; @@ -76,7 +82,7 @@ class InstallButton extends ConsumerWidget { children: [ Expanded(child: SizedBox(height: 48, child: button)), // Show action buttons only for installed apps with no active operation - if (isInstalled && operation == null) ...[ + if (canShowActionButtons) ...[ if (hasUpdate) ...[ const SizedBox(width: 8), _buildOpenIconButton(context, ref), @@ -101,9 +107,15 @@ class InstallButton extends ConsumerWidget { required FileMetadata? fileMetadata, required double fontSize, }) { + // Completed is a terminal "result" state and may linger for batch progress UX. + // If the app is no longer installed (e.g. user uninstalled right after install), + // ignore a stale Completed op so we fall back to the normal "Install" UI. + final effectiveOperation = + (!isInstalled && operation is Completed) ? null : operation; + // Handle operation states first - if (operation != null) { - return switch (operation) { + if (effectiveOperation != null) { + return switch (effectiveOperation) { DownloadQueued() => _buildSimpleButton( context, 'Queued for download',