fix: handle terminal install ops in install button

- Show uninstall (delete) actions immediately after install by treating Completed as not busy on the detail screen.
- Ignore stale Completed when the app is no longer installed so the primary CTA returns to Install.
- Allow install → uninstall → install again by clearing stale terminal ops (Completed/OperationFailed) in PackageManager.startDownload() (keeps batch progress UX intact).
This commit is contained in:
Henrique Velloso
2026-02-05 20:55:05 -03:00
parent 5acfa55898
commit eaf38e839b
2 changed files with 27 additions and 4 deletions
@@ -330,7 +330,18 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
String? displayName, String? displayName,
}) async { }) async {
await _ensureDownloaderReady(); 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; final downloadUrl = target.urls.firstOrNull;
if (downloadUrl == null || downloadUrl.isEmpty) { if (downloadUrl == null || downloadUrl.isEmpty) {
+15 -3
View File
@@ -31,6 +31,12 @@ class InstallButton extends ConsumerWidget {
// Derive what to display // Derive what to display
final isInstalled = installedPkg != null; 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 hasUpdate = app.hasUpdate;
final hasDowngrade = app.hasDowngrade; final hasDowngrade = app.hasDowngrade;
final hasRelease = release != null; final hasRelease = release != null;
@@ -76,7 +82,7 @@ class InstallButton extends ConsumerWidget {
children: [ children: [
Expanded(child: SizedBox(height: 48, child: button)), Expanded(child: SizedBox(height: 48, child: button)),
// Show action buttons only for installed apps with no active operation // Show action buttons only for installed apps with no active operation
if (isInstalled && operation == null) ...[ if (canShowActionButtons) ...[
if (hasUpdate) ...[ if (hasUpdate) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
_buildOpenIconButton(context, ref), _buildOpenIconButton(context, ref),
@@ -101,9 +107,15 @@ class InstallButton extends ConsumerWidget {
required FileMetadata? fileMetadata, required FileMetadata? fileMetadata,
required double fontSize, 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 // Handle operation states first
if (operation != null) { if (effectiveOperation != null) {
return switch (operation) { return switch (effectiveOperation) {
DownloadQueued() => _buildSimpleButton( DownloadQueued() => _buildSimpleButton(
context, context,
'Queued for download', 'Queued for download',