diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index e3b7aa3..54f8713 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/utils/debug_utils.dart b/lib/utils/debug_utils.dart index 3d0f60f..d45933e 100644 --- a/lib/utils/debug_utils.dart +++ b/lib/utils/debug_utils.dart @@ -11,6 +11,7 @@ bool isDebugMode(String? pubkey) { const allowedHexKeys = { '726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11', '227ce06aa5fb84bf70f25d887519374d1bfd3e1ffb75697bb0aa8f6396f32e43', + '24480686b56234a240fd9827209b584847f3d4f9657f0d9a97aec5320a264acb', }; // Try direct comparison first (hex format) diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 489f339..8aa0a51 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -32,6 +32,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; @@ -77,7 +83,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), @@ -102,9 +108,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',