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',