diff --git a/app/lib/services/dependency_manager.dart b/app/lib/services/dependency_manager.dart index d2959db..62b3d24 100644 --- a/app/lib/services/dependency_manager.dart +++ b/app/lib/services/dependency_manager.dart @@ -422,10 +422,12 @@ class DependencyManager { expectedSha256: platformInfo.sha256, ); - // Extract + // Extract. _extractZip emits per-file extraction progress; this initial + // event (0/0 -> indeterminate) covers the synchronous decode that precedes + // the first file write. _progressController.add(DownloadProgress( - bytesReceived: platformInfo.size ?? 0, - totalBytes: platformInfo.size ?? 0, + bytesReceived: 0, + totalBytes: 0, status: 'Extracting...', )); @@ -528,6 +530,28 @@ class DependencyManager { final bytes = await zipFile.readAsBytes(); final archive = ZipDecoder().decodeBytes(bytes); + // Total uncompressed size, for extraction progress. Extraction (many file + // writes + chmod) is slow on Linux, so report progress rather than leaving + // the dialog frozen on "Extracting...". + var totalBytes = 0; + for (final file in archive) { + if (file.isFile) totalBytes += file.size; + } + + var extracted = 0; + var lastEmitted = -1; + const emitEvery = 2 * 1024 * 1024; // throttle UI updates to ~every 2 MB + + void emitExtractProgress() { + _progressController.add(DownloadProgress( + bytesReceived: extracted, + totalBytes: totalBytes, + status: 'Extracting...', + )); + } + + emitExtractProgress(); + for (final file in archive) { final filePath = path.join(depsDir.path, file.name); @@ -540,11 +564,21 @@ class DependencyManager { if (!Platform.isWindows && _isExecutable(file.name)) { await Process.run('chmod', ['+x', filePath]); } + + extracted += file.size; + if (extracted - lastEmitted >= emitEvery) { + lastEmitted = extracted; + emitExtractProgress(); + } } else { await Directory(filePath).create(recursive: true); } } + // Final 100% extraction tick. + extracted = totalBytes; + emitExtractProgress(); + // On macOS, remove quarantine attribute and re-sign binaries for Gatekeeper if (Platform.isMacOS) { await _removeQuarantine(depsDir.path); diff --git a/app/lib/views/dependency_download_dialog.dart b/app/lib/views/dependency_download_dialog.dart index 6eac62e..2bdb69f 100644 --- a/app/lib/views/dependency_download_dialog.dart +++ b/app/lib/views/dependency_download_dialog.dart @@ -155,22 +155,26 @@ class _DependencyDownloadDialogState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(_progress!.status), - Text(_progress!.progressPercent), + if (_progress!.totalBytes > 0) Text(_progress!.progressPercent), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), + // Indeterminate (animated) when we don't yet have a fraction — + // e.g. connecting, or the zip-decode step before extraction + // byte progress starts — otherwise a determinate bar. child: LinearProgressIndicator( - value: _progress!.progress, + value: _progress!.progress > 0 ? _progress!.progress : null, minHeight: 8, ), ), const SizedBox(height: 8), - Text( - '${_formatBytes(_progress!.bytesReceived)} / ${_formatBytes(_progress!.totalBytes)}', - style: Theme.of(context).textTheme.bodySmall, - ), + if (_progress!.totalBytes > 0) + Text( + '${_formatBytes(_progress!.bytesReceived)} / ${_formatBytes(_progress!.totalBytes)}', + style: Theme.of(context).textTheme.bodySmall, + ), ] else ...[ const LinearProgressIndicator(), const SizedBox(height: 8),