@_nazmiforreal/flutter-ota 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/bin/flutter-ota.js +0 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  4. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +754 -0
  5. package/dart-src/packages/cli-tools/.dart_tool/version +1 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -1
  7. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +17 -8
  8. package/dart-src/packages/cli-tools/pubspec.lock +636 -0
  9. package/dart-src/packages/cli-tools/pubspec.yaml +7 -7
  10. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +2 -1
  11. package/dart-src/root/analysis_options.yaml +3 -0
  12. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +1 -0
  13. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +24 -5
  14. package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +20 -0
  15. package/dart-src/root/lib/flutter_ota_kit.dart +31 -11
  16. package/dart-src/root/lib/src/aws_update_source.dart +1 -0
  17. package/dart-src/root/lib/src/cloudflare_update_source.dart +1 -0
  18. package/dart-src/root/lib/src/ota_progress_overlay.dart +243 -296
  19. package/dart-src/root/lib/src/patcher_channel.dart +14 -1
  20. package/dart-src/root/lib/src/pkg/plugin_core/src/request_update_bundle_state.dart +2 -1
  21. package/dart-src/root/lib/src/pkg/postgres/src/postgres_types.dart +2 -1
  22. package/dart-src/root/lib/src/pocketbase_update_source.dart +1 -0
  23. package/dart-src/root/lib/src/postgres_update_source.dart +1 -0
  24. package/dart-src/root/lib/src/shared_update_check.dart +30 -2
  25. package/dart-src/root/lib/src/supabase_update_source.dart +1 -0
  26. package/dart-src/root/pubspec.yaml +14 -14
  27. package/package.json +1 -1
@@ -169,7 +169,7 @@ const Map<String, Color> _logColors = {
169
169
  /// - Title bar (traffic lights + status pill)
170
170
  /// - META (channel / version / bundle / size)
171
171
  /// - STEPS (per-phase status list)
172
- /// - PROGRESS (monospace bar + speed / eta / phase)
172
+ /// - PROGRESS (single-line determinate bar + percent)
173
173
  /// - LOG (scrolling colored feed)
174
174
  /// - Footer (deploy message, or error hint + retry)
175
175
  class OtaProgressOverlay extends StatefulWidget {
@@ -275,36 +275,35 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
275
275
 
276
276
  // ── Title bar ─────────────────────────────────────────────────────────
277
277
  Widget _titleBar(OtaOverlayState s) => Container(
278
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
279
- decoration: const BoxDecoration(
280
- color: _C.head,
281
- borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
282
- border: Border(bottom: BorderSide(color: _C.border)),
278
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
279
+ decoration: const BoxDecoration(
280
+ color: _C.head,
281
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
282
+ border: Border(bottom: BorderSide(color: _C.border)),
283
+ ),
284
+ child: Row(
285
+ children: [
286
+ _dot(_C.red),
287
+ const SizedBox(width: 6),
288
+ _dot(_C.yellow),
289
+ const SizedBox(width: 6),
290
+ _dot(_C.green),
291
+ const SizedBox(width: 12),
292
+ const Flexible(
293
+ child: Text(
294
+ 'flutter-ota · forced update',
295
+ overflow: TextOverflow.ellipsis,
296
+ style: TextStyle(color: _C.text, fontFamily: _mono, fontSize: 12),
297
+ ),
283
298
  ),
284
- child: Row(
285
- children: [
286
- _dot(_C.red),
287
- const SizedBox(width: 6),
288
- _dot(_C.yellow),
289
- const SizedBox(width: 6),
290
- _dot(_C.green),
291
- const SizedBox(width: 12),
292
- const Flexible(
293
- child: Text(
294
- 'flutter-ota · forced update',
295
- overflow: TextOverflow.ellipsis,
296
- style: TextStyle(
297
- color: _C.text, fontFamily: _mono, fontSize: 12),
298
- ),
299
- ),
300
- const SizedBox(width: 8),
301
- _pill(
302
- s.hasError ? 'FAILED' : 'RUNNING',
303
- s.hasError ? _C.red : _C.green,
304
- ),
305
- ],
299
+ const SizedBox(width: 8),
300
+ _pill(
301
+ s.hasError ? 'FAILED' : 'RUNNING',
302
+ s.hasError ? _C.red : _C.green,
306
303
  ),
307
- );
304
+ ],
305
+ ),
306
+ );
308
307
 
309
308
  // ── META ──────────────────────────────────────────────────────────────
310
309
  Widget _metaBlock(OtaOverlayState s) {
@@ -330,12 +329,15 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
330
329
  style: const TextStyle(fontFamily: _mono, fontSize: 13),
331
330
  children: [
332
331
  TextSpan(
333
- text: '${r[0].padRight(9)} ',
334
- style: const TextStyle(color: _C.dim)),
332
+ text: '${r[0].padRight(9)} ',
333
+ style: const TextStyle(color: _C.dim),
334
+ ),
335
335
  TextSpan(
336
- text: r[1],
337
- style: TextStyle(
338
- color: r[0] == 'version' ? _C.blue : _C.text)),
336
+ text: r[1],
337
+ style: TextStyle(
338
+ color: r[0] == 'version' ? _C.blue : _C.text,
339
+ ),
340
+ ),
339
341
  ],
340
342
  ),
341
343
  ),
@@ -367,31 +369,32 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
367
369
  final Color color = failed
368
370
  ? _C.red
369
371
  : done
370
- ? _C.green
371
- : active
372
- ? _C.blue
373
- : _C.faint;
372
+ ? _C.green
373
+ : active
374
+ ? _C.blue
375
+ : _C.faint;
374
376
 
375
377
  final String glyph = failed
376
378
  ? '✗'
377
379
  : done
378
- ? '✓'
379
- : active
380
- ? spinner
381
- : '·';
380
+ ? '✓'
381
+ : active
382
+ ? spinner
383
+ : '·';
382
384
 
383
385
  // Only the downloading step shows a percentage. Verify / install /
384
- // finalize have no meaningful fraction, so they show a live "working"
385
- // spinner instead of a frozen number — nothing looks hung.
386
+ // Only the download step shows a percentage. Other active phases show
387
+ // a steady "working…" (no spinning glyph in the status column — the
388
+ // row's leading glyph already spins), so nothing jitters.
386
389
  final String status;
387
390
  if (failed) {
388
391
  status = 'error';
389
392
  } else if (done) {
390
393
  status = 'done';
391
394
  } else if (active) {
392
- final showPct = s.phase == PatchApplyPhase.downloading &&
393
- s.fraction != null;
394
- status = showPct ? _pct(s.fraction) : 'working $spinner';
395
+ final showPct =
396
+ s.phase == PatchApplyPhase.downloading && s.fraction != null;
397
+ status = showPct ? _pct(s.fraction) : 'working…';
395
398
  } else {
396
399
  status = 'wait';
397
400
  }
@@ -402,9 +405,14 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
402
405
  children: [
403
406
  SizedBox(
404
407
  width: 18,
405
- child: Text(glyph,
406
- style: TextStyle(
407
- color: color, fontFamily: _mono, fontSize: 14)),
408
+ child: Text(
409
+ glyph,
410
+ style: TextStyle(
411
+ color: color,
412
+ fontFamily: _mono,
413
+ fontSize: 14,
414
+ ),
415
+ ),
408
416
  ),
409
417
  const SizedBox(width: 8),
410
418
  Expanded(
@@ -437,68 +445,72 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
437
445
 
438
446
  // ── PROGRESS ──────────────────────────────────────────────────────────
439
447
  Widget _progressBlock(OtaOverlayState s) {
440
- final speed = (s.bytesPerSec != null && s.bytesPerSec! > 0)
441
- ? '${(s.bytesPerSec! / 1048576).toStringAsFixed(1)} MB/s'
442
- : '—';
443
- String eta = '—';
444
- if (s.bytesPerSec != null &&
445
- s.bytesPerSec! > 0 &&
446
- s.totalBytes > s.bytesReceived) {
447
- final secs = ((s.totalBytes - s.bytesReceived) / s.bytesPerSec!).ceil();
448
- eta = secs >= 60 ? '${(secs / 60).ceil()}m' : '${secs}s';
448
+ // Single honest fraction for the bar:
449
+ // - preparing (no phase / no fraction yet) → empty
450
+ // - downloading with a known size → real fraction
451
+ // - verifying / finalizing / restarting → full (download is already
452
+ // done, so the bar rests at 100%)
453
+ // - error → freeze wherever it was
454
+ final double frac;
455
+ if (s.hasError) {
456
+ frac = (s.fraction ?? 0).clamp(0.0, 1.0);
457
+ } else if (s.restarting) {
458
+ frac = 1.0;
459
+ } else if (s.phase == PatchApplyPhase.downloading) {
460
+ frac = (s.fraction ?? 0).clamp(0.0, 1.0);
461
+ } else if (s.phase == PatchApplyPhase.verifying ||
462
+ s.phase == PatchApplyPhase.finalizing) {
463
+ frac = 1.0;
464
+ } else {
465
+ frac = 0.0; // preparing
466
+ }
467
+
468
+ // The readout lives on its own line above the bar (right aligned) so the
469
+ // bar itself spans the full panel width and can visibly reach the end.
470
+ // It used to sit in a fixed column beside the bar, which (a) reserved
471
+ // 54px the bar could never grow into and (b) hid the percent as soon as
472
+ // the download phase ended, making the bar look stuck below 100%.
473
+ final String pctLabel;
474
+ if (s.hasError) {
475
+ pctLabel = '';
476
+ } else if (s.restarting ||
477
+ s.phase == PatchApplyPhase.verifying ||
478
+ s.phase == PatchApplyPhase.finalizing) {
479
+ pctLabel = _pct(1.0);
480
+ } else if (s.phase == PatchApplyPhase.downloading && s.fraction != null) {
481
+ pctLabel = _pct(s.fraction);
482
+ } else {
483
+ pctLabel = '';
449
484
  }
450
- final phase = s.hasError
451
- ? 'halted'
452
- : (s.restarting ? 'restarting' : (s.phase?.name ?? 'preparing'));
453
-
454
- // A determinate fraction is only meaningful while downloading with a known
455
- // Content-Length. Verifying / finalizing / unknown-size downloads render an
456
- // indeterminate (sliding) bar so nothing ever looks stuck at 0%.
457
- final bool determinate = !s.hasError &&
458
- s.phase == PatchApplyPhase.downloading &&
459
- s.fraction != null;
460
- final double? frac = s.hasError
461
- ? (s.fraction ?? 0)
462
- : (determinate ? s.fraction : null);
463
485
 
464
486
  return _panelBox(
465
487
  child: Column(
466
- crossAxisAlignment: CrossAxisAlignment.start,
488
+ key: const Key('ota-progress-panel'),
489
+ crossAxisAlignment: CrossAxisAlignment.stretch,
467
490
  children: [
468
- Row(
469
- children: [
470
- // The bar is a real widget (never wraps), and animates smoothly
471
- // between values instead of snapping.
472
- Expanded(
473
- child: _ProgressBar(
474
- fraction: frac,
475
- color: s.hasError ? _C.red : _C.blue,
476
- ),
477
- ),
478
- const SizedBox(width: 10),
479
- SizedBox(
480
- width: 44,
481
- child: Text(
482
- determinate ? _pct(s.fraction) : '',
483
- textAlign: TextAlign.right,
484
- style: const TextStyle(
485
- fontFamily: _mono,
486
- fontSize: 13,
487
- color: _C.text,
488
- fontWeight: FontWeight.w700,
489
- fontFeatures: [FontFeature.tabularFigures()],
490
- ),
491
+ SizedBox(
492
+ height: 16,
493
+ child: Align(
494
+ alignment: Alignment.centerRight,
495
+ child: Text(
496
+ pctLabel,
497
+ style: const TextStyle(
498
+ fontFamily: _mono,
499
+ fontSize: 13,
500
+ color: _C.text,
501
+ fontWeight: FontWeight.w700,
502
+ fontFeatures: [FontFeature.tabularFigures()],
491
503
  ),
492
504
  ),
493
- ],
505
+ ),
494
506
  ),
495
- const SizedBox(height: 12),
496
- Row(
497
- children: [
498
- _stat('speed', speed),
499
- _stat('eta', eta),
500
- _stat('phase', phase),
501
- ],
507
+ const SizedBox(height: 4),
508
+ SizedBox(
509
+ key: const Key('ota-progress-bar'),
510
+ child: _ProgressBar(
511
+ fraction: frac,
512
+ color: s.hasError ? _C.red : _C.blue,
513
+ ),
502
514
  ),
503
515
  ],
504
516
  ),
@@ -536,7 +548,10 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
536
548
  child: Text(
537
549
  s.errorHint ?? 'Close the app and reopen to retry.',
538
550
  style: const TextStyle(
539
- color: _C.dim, fontFamily: _mono, fontSize: 12),
551
+ color: _C.dim,
552
+ fontFamily: _mono,
553
+ fontSize: 12,
554
+ ),
540
555
  ),
541
556
  ),
542
557
  if (showRetry)
@@ -544,25 +559,35 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
544
559
  onTap: widget.onRetry,
545
560
  child: Container(
546
561
  padding: const EdgeInsets.symmetric(
547
- horizontal: 14, vertical: 7),
562
+ horizontal: 14,
563
+ vertical: 7,
564
+ ),
548
565
  decoration: BoxDecoration(
549
566
  borderRadius: BorderRadius.circular(7),
550
567
  border: Border.all(color: _C.border),
551
568
  ),
552
- child: const Text('[ retry ]',
553
- style: TextStyle(
554
- color: _C.blue,
555
- fontFamily: _mono,
556
- fontSize: 13)),
569
+ child: const Text(
570
+ '[ retry ]',
571
+ style: TextStyle(
572
+ color: _C.blue,
573
+ fontFamily: _mono,
574
+ fontSize: 13,
575
+ ),
576
+ ),
557
577
  ),
558
578
  ),
559
579
  ],
560
580
  )
561
581
  : Row(
562
582
  children: [
563
- const Text('›',
564
- style: TextStyle(
565
- color: _C.green, fontFamily: _mono, fontSize: 13)),
583
+ const Text(
584
+ '›',
585
+ style: TextStyle(
586
+ color: _C.green,
587
+ fontFamily: _mono,
588
+ fontSize: 13,
589
+ ),
590
+ ),
566
591
  const SizedBox(width: 8),
567
592
  Expanded(
568
593
  child: Text(
@@ -570,7 +595,10 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
570
595
  ? s.message!
571
596
  : 'Applying update, please keep the app open…',
572
597
  style: const TextStyle(
573
- color: _C.dim, fontFamily: _mono, fontSize: 12),
598
+ color: _C.dim,
599
+ fontFamily: _mono,
600
+ fontSize: 12,
601
+ ),
574
602
  ),
575
603
  ),
576
604
  ],
@@ -580,83 +608,68 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
580
608
 
581
609
  // ── helpers ─────────────────────────────────────────────────────────
582
610
  Widget _sectionLabel(String t) => Row(
583
- children: [
584
- Text(t,
585
- style: const TextStyle(
586
- color: _C.dim,
587
- fontFamily: _mono,
588
- fontSize: 11,
589
- fontWeight: FontWeight.w700,
590
- letterSpacing: 1.5,
591
- )),
592
- const SizedBox(width: 10),
593
- const Expanded(child: Divider(color: _C.sep, height: 1)),
594
- ],
595
- );
611
+ children: [
612
+ Text(
613
+ t,
614
+ style: const TextStyle(
615
+ color: _C.dim,
616
+ fontFamily: _mono,
617
+ fontSize: 11,
618
+ fontWeight: FontWeight.w700,
619
+ letterSpacing: 1.5,
620
+ ),
621
+ ),
622
+ const SizedBox(width: 10),
623
+ const Expanded(child: Divider(color: _C.sep, height: 1)),
624
+ ],
625
+ );
596
626
 
597
627
  Widget _panelBox({required Widget child}) => Container(
598
- width: double.infinity,
599
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
600
- decoration: BoxDecoration(
601
- color: _C.inner,
602
- borderRadius: BorderRadius.circular(8),
603
- border: Border.all(color: _C.sep),
604
- ),
605
- child: child,
606
- );
607
-
608
- Widget _stat(String k, String v) => Expanded(
609
- child: Column(
610
- crossAxisAlignment: CrossAxisAlignment.start,
611
- children: [
612
- Text(k,
613
- style: const TextStyle(
614
- color: _C.faint, fontFamily: _mono, fontSize: 10)),
615
- const SizedBox(height: 2),
616
- Text(v,
617
- overflow: TextOverflow.ellipsis,
618
- style: const TextStyle(
619
- color: _C.text, fontFamily: _mono, fontSize: 12)),
620
- ],
621
- ),
622
- );
628
+ width: double.infinity,
629
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
630
+ decoration: BoxDecoration(
631
+ color: _C.inner,
632
+ borderRadius: BorderRadius.circular(8),
633
+ border: Border.all(color: _C.sep),
634
+ ),
635
+ child: child,
636
+ );
623
637
 
624
638
  Widget _dot(Color c) => Container(
625
- width: 11,
626
- height: 11,
627
- decoration: BoxDecoration(color: c, shape: BoxShape.circle),
628
- );
639
+ width: 11,
640
+ height: 11,
641
+ decoration: BoxDecoration(color: c, shape: BoxShape.circle),
642
+ );
629
643
 
630
644
  Widget _pill(String t, Color c) => Container(
631
- padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
632
- decoration: BoxDecoration(
633
- color: c.withValues(alpha: 0.15),
634
- borderRadius: BorderRadius.circular(6),
635
- border: Border.all(color: c.withValues(alpha: 0.5)),
636
- ),
637
- child: Text(t,
638
- style: TextStyle(
639
- color: c,
640
- fontFamily: _mono,
641
- fontSize: 10,
642
- fontWeight: FontWeight.w700,
643
- letterSpacing: 0.5)),
644
- );
645
+ padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
646
+ decoration: BoxDecoration(
647
+ color: c.withValues(alpha: 0.15),
648
+ borderRadius: BorderRadius.circular(6),
649
+ border: Border.all(color: c.withValues(alpha: 0.5)),
650
+ ),
651
+ child: Text(
652
+ t,
653
+ style: TextStyle(
654
+ color: c,
655
+ fontFamily: _mono,
656
+ fontSize: 10,
657
+ fontWeight: FontWeight.w700,
658
+ letterSpacing: 0.5,
659
+ ),
660
+ ),
661
+ );
645
662
  }
646
663
 
647
- /// A single-line progress bar that never wraps.
648
- ///
649
- /// - When [fraction] is non-null it's a determinate bar that animates smoothly
650
- /// between values (no snapping / jumping).
651
- /// - When [fraction] is null it's an indeterminate sliding bar, so
652
- /// verify/install/finalize (which have no meaningful percentage) never look
653
- /// frozen at 0%.
664
+ /// A single-line determinate progress bar that never wraps.
654
665
  ///
655
666
  /// It's a real painted widget with a fixed height inside an [Expanded], so it
656
667
  /// always stays on one line regardless of width — unlike the old
657
- /// character-string bar, which wrapped when it didn't fit.
668
+ /// character-string bar, which wrapped when it didn't fit. It eases toward its
669
+ /// target (forward-only) instead of snapping, and has no indeterminate sliding
670
+ /// mode — once a download completes the bar simply rests at 100%.
658
671
  class _ProgressBar extends StatefulWidget {
659
- final double? fraction;
672
+ final double fraction;
660
673
  final Color color;
661
674
 
662
675
  const _ProgressBar({required this.fraction, required this.color});
@@ -665,40 +678,22 @@ class _ProgressBar extends StatefulWidget {
665
678
  State<_ProgressBar> createState() => _ProgressBarState();
666
679
  }
667
680
 
668
- class _ProgressBarState extends State<_ProgressBar>
669
- with SingleTickerProviderStateMixin {
670
- late final AnimationController _indeterminate = AnimationController(
671
- vsync: this,
672
- duration: const Duration(milliseconds: 1200),
673
- );
674
-
675
- // Smoothed determinate value so the bar eases toward the target instead of
676
- // snapping. Monotonic — it never visually goes backwards.
681
+ class _ProgressBarState extends State<_ProgressBar> {
682
+ // Smoothed value so the bar eases toward the target. Forward-only — it never
683
+ // visually goes backwards.
677
684
  double _shown = 0;
678
685
 
679
686
  @override
680
687
  void initState() {
681
688
  super.initState();
682
- _shown = widget.fraction ?? 0;
683
- if (widget.fraction == null) _indeterminate.repeat();
689
+ _shown = widget.fraction.clamp(0.0, 1.0);
684
690
  }
685
691
 
686
692
  @override
687
693
  void didUpdateWidget(covariant _ProgressBar old) {
688
694
  super.didUpdateWidget(old);
689
- if (widget.fraction == null) {
690
- if (!_indeterminate.isAnimating) _indeterminate.repeat();
691
- } else {
692
- if (_indeterminate.isAnimating) _indeterminate.stop();
693
- final target = widget.fraction!.clamp(0.0, 1.0);
694
- if (target >= _shown) _shown = target; // forward-only
695
- }
696
- }
697
-
698
- @override
699
- void dispose() {
700
- _indeterminate.dispose();
701
- super.dispose();
695
+ final target = widget.fraction.clamp(0.0, 1.0);
696
+ if (target >= _shown) _shown = target;
702
697
  }
703
698
 
704
699
  static const _height = 8.0;
@@ -712,60 +707,24 @@ class _ProgressBarState extends State<_ProgressBar>
712
707
  height: _height,
713
708
  child: ColoredBox(
714
709
  color: _track,
715
- child: widget.fraction == null
716
- ? _indeterminateBar()
717
- : _determinateBar(),
718
- ),
719
- ),
720
- );
721
- }
722
-
723
- Widget _determinateBar() {
724
- return LayoutBuilder(
725
- builder: (context, c) => TweenAnimationBuilder<double>(
726
- // Ease toward the smoothed target; short duration keeps it responsive
727
- // but never snappy.
728
- tween: Tween(end: _shown),
729
- duration: const Duration(milliseconds: 400),
730
- curve: Curves.easeOut,
731
- builder: (context, v, _) => Align(
732
- alignment: Alignment.centerLeft,
733
- child: Container(
734
- width: (c.maxWidth * v.clamp(0.0, 1.0)),
735
- decoration: BoxDecoration(
736
- color: widget.color,
737
- borderRadius: BorderRadius.circular(99),
738
- ),
739
- ),
740
- ),
741
- ),
742
- );
743
- }
744
-
745
- Widget _indeterminateBar() {
746
- return LayoutBuilder(
747
- builder: (context, c) => AnimatedBuilder(
748
- animation: _indeterminate,
749
- builder: (context, _) {
750
- const segment = 0.35; // 35% wide moving segment
751
- final t = _indeterminate.value; // 0..1
752
- final left = (t * (1 + segment)) - segment; // slides in from the left
753
- return Align(
754
- alignment: Alignment.centerLeft,
755
- child: Padding(
756
- padding: EdgeInsets.only(
757
- left: (left.clamp(0.0, 1.0)) * c.maxWidth,
758
- ),
759
- child: Container(
760
- width: segment * c.maxWidth,
761
- decoration: BoxDecoration(
762
- color: widget.color,
763
- borderRadius: BorderRadius.circular(99),
710
+ child: LayoutBuilder(
711
+ builder: (context, c) => TweenAnimationBuilder<double>(
712
+ tween: Tween(end: _shown),
713
+ duration: const Duration(milliseconds: 300),
714
+ curve: Curves.easeOut,
715
+ builder: (context, v, _) => Align(
716
+ alignment: Alignment.centerLeft,
717
+ child: Container(
718
+ width: c.maxWidth * v.clamp(0.0, 1.0),
719
+ decoration: BoxDecoration(
720
+ color: widget.color,
721
+ borderRadius: BorderRadius.circular(99),
722
+ ),
764
723
  ),
765
724
  ),
766
725
  ),
767
- );
768
- },
726
+ ),
727
+ ),
769
728
  ),
770
729
  );
771
730
  }
@@ -830,19 +789,20 @@ class _TerminalLogState extends State<_TerminalLog> {
830
789
  style: const TextStyle(fontFamily: _mono, fontSize: 12),
831
790
  children: [
832
791
  TextSpan(
833
- text: '${match.group(1)!.padRight(8)} ',
834
- style: TextStyle(color: color)),
792
+ text: '${match.group(1)!.padRight(8)} ',
793
+ style: TextStyle(color: color),
794
+ ),
835
795
  TextSpan(
836
- text: match.group(3),
837
- style: const TextStyle(color: _C.text)),
796
+ text: match.group(3),
797
+ style: const TextStyle(color: _C.text),
798
+ ),
838
799
  ],
839
800
  ),
840
801
  );
841
802
  } else {
842
803
  content = Text(
843
804
  line.text,
844
- style: TextStyle(
845
- fontFamily: _mono, fontSize: 12, color: color),
805
+ style: TextStyle(fontFamily: _mono, fontSize: 12, color: color),
846
806
  );
847
807
  }
848
808
 
@@ -869,9 +829,13 @@ class OtaOverlayManager {
869
829
  bool _disposed = false;
870
830
  DateTime? _startTime;
871
831
 
872
- // Download-speed tracking.
873
- int _lastBytes = 0;
874
- DateTime? _lastBytesAt;
832
+ /// Completes once the active overlay entry has been removed. [begin] resets
833
+ /// it for every session; `FlutterPatcher.applyUpdate` awaits it so the
834
+ /// forced-update restart can never kill a still-visible install screen.
835
+ Completer<void>? _dismissed;
836
+
837
+ Future<void> get _dismissedFuture =>
838
+ _dismissed?.future ?? Future<void>.value();
875
839
 
876
840
  /// Last 10%-bucket we emitted a "download NN%" log line for, to throttle the
877
841
  /// feed. -1 = none yet.
@@ -911,9 +875,8 @@ class OtaOverlayManager {
911
875
  if (overlay == null) return null;
912
876
 
913
877
  _startTime = DateTime.now();
914
- _lastBytes = 0;
915
- _lastBytesAt = null;
916
878
  _lastLoggedPctBucket = -1;
879
+ _dismissed = Completer<void>();
917
880
 
918
881
  final logs = <LogLine>[
919
882
  if (channel != null) LogLine('gray', 'channel $channel'),
@@ -984,30 +947,6 @@ class OtaOverlayManager {
984
947
  final phaseStep = _phaseToStep(progress.phase);
985
948
  final newStep = phaseStep < s.activeStep ? s.activeStep : phaseStep;
986
949
 
987
- // Compute a smoothed download speed from byte deltas. A single sample can
988
- // spike wildly (buffered reads), so we blend with the previous value.
989
- int? bytesPerSec = s.bytesPerSec;
990
- if (progress.phase == PatchApplyPhase.downloading &&
991
- progress.bytesReceived > 0) {
992
- final now = DateTime.now();
993
- if (_lastBytesAt != null && progress.bytesReceived > _lastBytes) {
994
- final dt = now.difference(_lastBytesAt!).inMilliseconds / 1000.0;
995
- if (dt > 0.05) {
996
- final sample =
997
- ((progress.bytesReceived - _lastBytes) / dt).round();
998
- // Exponential moving average → the speed/ETA read-out stops jumping.
999
- bytesPerSec = bytesPerSec == null || bytesPerSec == 0
1000
- ? sample
1001
- : (bytesPerSec * 0.6 + sample * 0.4).round();
1002
- _lastBytes = progress.bytesReceived;
1003
- _lastBytesAt = now;
1004
- }
1005
- } else {
1006
- _lastBytes = progress.bytesReceived;
1007
- _lastBytesAt = now;
1008
- }
1009
- }
1010
-
1011
950
  // Log phase transitions once, with staggered detail lines.
1012
951
  if (s.phase != progress.phase) {
1013
952
  switch (progress.phase) {
@@ -1048,7 +987,6 @@ class OtaOverlayManager {
1048
987
  fraction: progress.fraction,
1049
988
  bytesReceived: progress.bytesReceived,
1050
989
  totalBytes: progress.totalBytes > 0 ? progress.totalBytes : null,
1051
- bytesPerSec: bytesPerSec,
1052
990
  activeStep: newStep,
1053
991
  );
1054
992
  }
@@ -1062,7 +1000,12 @@ class OtaOverlayManager {
1062
1000
  }
1063
1001
 
1064
1002
  void _end({bool hasError = false, String? errorText}) {
1065
- if (_disposed || _entry == null) return;
1003
+ if (_disposed || _entry == null) {
1004
+ if (_dismissed != null && !_dismissed!.isCompleted) {
1005
+ _dismissed!.complete();
1006
+ }
1007
+ return;
1008
+ }
1066
1009
  if (hasError) {
1067
1010
  _log('red', 'error ${errorText ?? 'unknown'}');
1068
1011
  _log('yellow', 'action rolling back to base apk');
@@ -1096,6 +1039,9 @@ class OtaOverlayManager {
1096
1039
  if (_disposed) return;
1097
1040
  entry?.remove();
1098
1041
  if (identical(_entry, entry)) _entry = null;
1042
+ if (_dismissed != null && !_dismissed!.isCompleted) {
1043
+ _dismissed!.complete();
1044
+ }
1099
1045
  });
1100
1046
  }
1101
1047
 
@@ -1150,10 +1096,7 @@ class OtaOverlayManager {
1150
1096
  ),
1151
1097
  Text(
1152
1098
  'Restored: $previousVersion',
1153
- style: TextStyle(
1154
- color: Colors.grey[400],
1155
- fontSize: 12,
1156
- ),
1099
+ style: TextStyle(color: Colors.grey[400], fontSize: 12),
1157
1100
  ),
1158
1101
  ],
1159
1102
  ),
@@ -1183,4 +1126,8 @@ class OtaOverlayHandle {
1183
1126
 
1184
1127
  void end({bool hasError = false, String? errorText}) =>
1185
1128
  _manager._end(hasError: hasError, errorText: errorText);
1129
+
1130
+ /// Completes when the overlay has fully unmounted (dwell elapsed + removed),
1131
+ /// or immediately if no overlay session is active.
1132
+ Future<void> get dismissed => _manager._dismissedFuture;
1186
1133
  }