@_nazmiforreal/flutter-ota 0.1.27 → 0.1.29

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.
@@ -30,6 +30,10 @@ class OtaOverlayState {
30
30
  final List<LogLine> logs;
31
31
  final bool canRetry;
32
32
 
33
+ /// True once the patch is installed and the app is about to cold-restart.
34
+ /// Drives the final "restarting" phase label + the completed steps list.
35
+ final bool restarting;
36
+
33
37
  /// Which step index is currently active (0-based). Steps that are done
34
38
  /// get a check; the active step gets the braille spinner.
35
39
  final int activeStep;
@@ -53,6 +57,7 @@ class OtaOverlayState {
53
57
  this.totalBytes = 0,
54
58
  this.logs = const [],
55
59
  this.canRetry = false,
60
+ this.restarting = false,
56
61
  this.activeStep = 0,
57
62
  });
58
63
 
@@ -74,6 +79,7 @@ class OtaOverlayState {
74
79
  int? totalBytes,
75
80
  List<LogLine>? logs,
76
81
  bool? canRetry,
82
+ bool? restarting,
77
83
  int? activeStep,
78
84
  }) {
79
85
  return OtaOverlayState(
@@ -95,6 +101,7 @@ class OtaOverlayState {
95
101
  totalBytes: totalBytes ?? this.totalBytes,
96
102
  logs: logs ?? this.logs,
97
103
  canRetry: canRetry ?? this.canRetry,
104
+ restarting: restarting ?? this.restarting,
98
105
  activeStep: activeStep ?? this.activeStep,
99
106
  );
100
107
  }
@@ -162,7 +169,7 @@ const Map<String, Color> _logColors = {
162
169
  /// - Title bar (traffic lights + status pill)
163
170
  /// - META (channel / version / bundle / size)
164
171
  /// - STEPS (per-phase status list)
165
- /// - PROGRESS (monospace bar + speed / eta / phase)
172
+ /// - PROGRESS (single-line determinate bar + percent)
166
173
  /// - LOG (scrolling colored feed)
167
174
  /// - Footer (deploy message, or error hint + retry)
168
175
  class OtaProgressOverlay extends StatefulWidget {
@@ -341,14 +348,21 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
341
348
  // ── STEPS ─────────────────────────────────────────────────────────────
342
349
  Widget _stepsBlock(OtaOverlayState s) {
343
350
  final spinner = _braille[_spinnerIndex];
351
+ // When restarting, every step is complete. Otherwise the active index is
352
+ // clamped into range so an out-of-bounds activeStep can never leave the
353
+ // list looking stuck or skip the visible progression.
354
+ final int activeIndex = s.restarting
355
+ ? _stepLabels.length
356
+ : s.activeStep.clamp(0, _stepLabels.length - 1);
357
+
344
358
  return _panelBox(
345
359
  child: Column(
346
360
  crossAxisAlignment: CrossAxisAlignment.start,
347
361
  children: List.generate(_stepLabels.length, (i) {
348
- final done = i < s.activeStep;
349
- final active = i == s.activeStep && !s.hasError;
350
- final failed = i == s.activeStep && s.hasError;
351
- final pending = i > s.activeStep;
362
+ final done = i < activeIndex;
363
+ final active = i == activeIndex && !s.hasError && !s.restarting;
364
+ final failed = i == activeIndex && s.hasError;
365
+ final pending = i > activeIndex;
352
366
 
353
367
  final Color color = failed
354
368
  ? _C.red
@@ -366,13 +380,22 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
366
380
  ? spinner
367
381
  : '·';
368
382
 
369
- final String status = failed
370
- ? 'error'
371
- : done
372
- ? 'done'
373
- : active
374
- ? _pct(s.fraction)
375
- : 'wait';
383
+ // Only the downloading step shows a percentage. Verify / install /
384
+ // Only the download step shows a percentage. Other active phases show
385
+ // a steady "working…" (no spinning glyph in the status column — the
386
+ // row's leading glyph already spins), so nothing jitters.
387
+ final String status;
388
+ if (failed) {
389
+ status = 'error';
390
+ } else if (done) {
391
+ status = 'done';
392
+ } else if (active) {
393
+ final showPct = s.phase == PatchApplyPhase.downloading &&
394
+ s.fraction != null;
395
+ status = showPct ? _pct(s.fraction) : 'working…';
396
+ } else {
397
+ status = 'wait';
398
+ }
376
399
 
377
400
  return Padding(
378
401
  padding: const EdgeInsets.symmetric(vertical: 3),
@@ -415,50 +438,53 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
415
438
 
416
439
  // ── PROGRESS ──────────────────────────────────────────────────────────
417
440
  Widget _progressBlock(OtaOverlayState s) {
418
- const total = 34;
419
- final frac = (s.fraction ?? 0).clamp(0.0, 1.0);
420
- final filled = (frac * total).round();
421
- final bar = '${'█' * filled}${'░' * (total - filled)}';
422
-
423
- final speed = (s.bytesPerSec != null && s.bytesPerSec! > 0)
424
- ? '${(s.bytesPerSec! / 1048576).toStringAsFixed(1)} MB/s'
425
- : '—';
426
- String eta = '—';
427
- if (s.bytesPerSec != null &&
428
- s.bytesPerSec! > 0 &&
429
- s.totalBytes > s.bytesReceived) {
430
- final secs = ((s.totalBytes - s.bytesReceived) / s.bytesPerSec!).ceil();
431
- eta = '${secs}s';
441
+ // Single honest fraction for the bar:
442
+ // - preparing (no phase / no fraction yet) → empty
443
+ // - downloading with a known size → real fraction
444
+ // - verifying / installing / finalizing / restarting → full (download is
445
+ // already done, so the bar simply stays at 100% — no sliding animation)
446
+ // - error → freeze wherever it was
447
+ final double frac;
448
+ if (s.hasError) {
449
+ frac = (s.fraction ?? 0).clamp(0.0, 1.0);
450
+ } else if (s.restarting) {
451
+ frac = 1.0;
452
+ } else if (s.phase == PatchApplyPhase.downloading) {
453
+ frac = (s.fraction ?? 0).clamp(0.0, 1.0);
454
+ } else if (s.phase == PatchApplyPhase.verifying ||
455
+ s.phase == PatchApplyPhase.finalizing) {
456
+ frac = 1.0;
457
+ } else {
458
+ frac = 0.0; // preparing
432
459
  }
433
- final phase = s.hasError
434
- ? 'halted'
435
- : (s.phase?.name ?? 'preparing');
460
+
461
+ // Percent read-out only while it's a real download percentage.
462
+ final bool showPct =
463
+ !s.hasError && s.phase == PatchApplyPhase.downloading && s.fraction != null;
436
464
 
437
465
  return _panelBox(
438
- child: Column(
439
- crossAxisAlignment: CrossAxisAlignment.start,
466
+ child: Row(
440
467
  children: [
441
- RichText(
442
- text: TextSpan(
443
- style: const TextStyle(fontFamily: _mono, fontSize: 13),
444
- children: [
445
- TextSpan(
446
- text: bar,
447
- style: TextStyle(color: s.hasError ? _C.red : _C.blue)),
448
- TextSpan(
449
- text: ' ${_pct(s.fraction)}',
450
- style: const TextStyle(
451
- color: _C.text, fontWeight: FontWeight.w700)),
452
- ],
468
+ Expanded(
469
+ child: _ProgressBar(
470
+ fraction: frac,
471
+ color: s.hasError ? _C.red : _C.blue,
453
472
  ),
454
473
  ),
455
- const SizedBox(height: 10),
456
- Row(
457
- children: [
458
- _stat('speed', speed),
459
- _stat('eta', eta),
460
- _stat('phase', phase),
461
- ],
474
+ const SizedBox(width: 10),
475
+ SizedBox(
476
+ width: 44,
477
+ child: Text(
478
+ showPct ? _pct(s.fraction) : '',
479
+ textAlign: TextAlign.right,
480
+ style: const TextStyle(
481
+ fontFamily: _mono,
482
+ fontSize: 13,
483
+ color: _C.text,
484
+ fontWeight: FontWeight.w700,
485
+ fontFeatures: [FontFeature.tabularFigures()],
486
+ ),
487
+ ),
462
488
  ),
463
489
  ],
464
490
  ),
@@ -565,22 +591,6 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
565
591
  child: child,
566
592
  );
567
593
 
568
- Widget _stat(String k, String v) => Expanded(
569
- child: Column(
570
- crossAxisAlignment: CrossAxisAlignment.start,
571
- children: [
572
- Text(k,
573
- style: const TextStyle(
574
- color: _C.faint, fontFamily: _mono, fontSize: 10)),
575
- const SizedBox(height: 2),
576
- Text(v,
577
- overflow: TextOverflow.ellipsis,
578
- style: const TextStyle(
579
- color: _C.text, fontFamily: _mono, fontSize: 12)),
580
- ],
581
- ),
582
- );
583
-
584
594
  Widget _dot(Color c) => Container(
585
595
  width: 11,
586
596
  height: 11,
@@ -604,6 +614,75 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
604
614
  );
605
615
  }
606
616
 
617
+ /// A single-line determinate progress bar that never wraps.
618
+ ///
619
+ /// It's a real painted widget with a fixed height inside an [Expanded], so it
620
+ /// always stays on one line regardless of width — unlike the old
621
+ /// character-string bar, which wrapped when it didn't fit. It eases toward its
622
+ /// target (forward-only) instead of snapping, and has no indeterminate sliding
623
+ /// mode — once a download completes the bar simply rests at 100%.
624
+ class _ProgressBar extends StatefulWidget {
625
+ final double fraction;
626
+ final Color color;
627
+
628
+ const _ProgressBar({required this.fraction, required this.color});
629
+
630
+ @override
631
+ State<_ProgressBar> createState() => _ProgressBarState();
632
+ }
633
+
634
+ class _ProgressBarState extends State<_ProgressBar> {
635
+ // Smoothed value so the bar eases toward the target. Forward-only — it never
636
+ // visually goes backwards.
637
+ double _shown = 0;
638
+
639
+ @override
640
+ void initState() {
641
+ super.initState();
642
+ _shown = widget.fraction.clamp(0.0, 1.0);
643
+ }
644
+
645
+ @override
646
+ void didUpdateWidget(covariant _ProgressBar old) {
647
+ super.didUpdateWidget(old);
648
+ final target = widget.fraction.clamp(0.0, 1.0);
649
+ if (target >= _shown) _shown = target;
650
+ }
651
+
652
+ static const _height = 8.0;
653
+ static const _track = Color(0xFF161C24);
654
+
655
+ @override
656
+ Widget build(BuildContext context) {
657
+ return ClipRRect(
658
+ borderRadius: BorderRadius.circular(99),
659
+ child: SizedBox(
660
+ height: _height,
661
+ child: ColoredBox(
662
+ color: _track,
663
+ child: LayoutBuilder(
664
+ builder: (context, c) => TweenAnimationBuilder<double>(
665
+ tween: Tween(end: _shown),
666
+ duration: const Duration(milliseconds: 300),
667
+ curve: Curves.easeOut,
668
+ builder: (context, v, _) => Align(
669
+ alignment: Alignment.centerLeft,
670
+ child: Container(
671
+ width: c.maxWidth * v.clamp(0.0, 1.0),
672
+ decoration: BoxDecoration(
673
+ color: widget.color,
674
+ borderRadius: BorderRadius.circular(99),
675
+ ),
676
+ ),
677
+ ),
678
+ ),
679
+ ),
680
+ ),
681
+ ),
682
+ );
683
+ }
684
+ }
685
+
607
686
  /// Scrollable terminal log view — auto-scrolls to bottom on new lines.
608
687
  ///
609
688
  /// A line whose text has a leading token followed by 2+ spaces is rendered
@@ -702,9 +781,9 @@ class OtaOverlayManager {
702
781
  bool _disposed = false;
703
782
  DateTime? _startTime;
704
783
 
705
- // Download-speed tracking.
706
- int _lastBytes = 0;
707
- DateTime? _lastBytesAt;
784
+ /// Last 10%-bucket we emitted a "download NN%" log line for, to throttle the
785
+ /// feed. -1 = none yet.
786
+ int _lastLoggedPctBucket = -1;
708
787
 
709
788
  OverlayState? Function()? _resolver;
710
789
 
@@ -740,8 +819,7 @@ class OtaOverlayManager {
740
819
  if (overlay == null) return null;
741
820
 
742
821
  _startTime = DateTime.now();
743
- _lastBytes = 0;
744
- _lastBytesAt = null;
822
+ _lastLoggedPctBucket = -1;
745
823
 
746
824
  final logs = <LogLine>[
747
825
  if (channel != null) LogLine('gray', 'channel $channel'),
@@ -775,9 +853,13 @@ class OtaOverlayManager {
775
853
  void _log(String color, String text) {
776
854
  if (_disposed) return;
777
855
  final current = _state.value;
778
- _state.value = current.copyWith(
779
- logs: [...current.logs, LogLine(color, text)],
780
- );
856
+ final next = [...current.logs, LogLine(color, text)];
857
+ // Cap the feed so a long session can't grow it without bound.
858
+ const maxLines = 200;
859
+ final capped = next.length > maxLines
860
+ ? next.sublist(next.length - maxLines)
861
+ : next;
862
+ _state.value = current.copyWith(logs: capped);
781
863
  }
782
864
 
783
865
  /// Map phase to step index.
@@ -802,24 +884,13 @@ class OtaOverlayManager {
802
884
  ? '${(pct.clamp(0.0, 1.0) * 100).toStringAsFixed(0)}%'
803
885
  : '';
804
886
 
805
- final newStep = _phaseToStep(progress.phase);
887
+ // Step index derived from phase, but forced to move only forward so an
888
+ // out-of-order or duplicate native event can never make the steps jump
889
+ // backwards or flicker.
890
+ final phaseStep = _phaseToStep(progress.phase);
891
+ final newStep = phaseStep < s.activeStep ? s.activeStep : phaseStep;
806
892
 
807
- // Compute download speed from byte deltas.
808
- int? bytesPerSec = s.bytesPerSec;
809
- if (progress.phase == PatchApplyPhase.downloading &&
810
- progress.bytesReceived > 0) {
811
- final now = DateTime.now();
812
- if (_lastBytesAt != null && progress.bytesReceived > _lastBytes) {
813
- final dt = now.difference(_lastBytesAt!).inMilliseconds / 1000.0;
814
- if (dt > 0) {
815
- bytesPerSec = ((progress.bytesReceived - _lastBytes) / dt).round();
816
- }
817
- }
818
- _lastBytes = progress.bytesReceived;
819
- _lastBytesAt = now;
820
- }
821
-
822
- // Log phase transitions with staggered delays.
893
+ // Log phase transitions once, with staggered detail lines.
823
894
  if (s.phase != progress.phase) {
824
895
  switch (progress.phase) {
825
896
  case PatchApplyPhase.downloading:
@@ -841,12 +912,17 @@ class OtaOverlayManager {
841
912
  }
842
913
  }
843
914
 
844
- // Log download progress with size.
915
+ // Throttle the "download NN%" log so a fast download doesn't spam dozens of
916
+ // lines (which made the feed scroll frantically). One line per ~10%.
845
917
  if (progress.phase == PatchApplyPhase.downloading &&
846
918
  pctText.isNotEmpty &&
847
919
  progress.totalBytes > 0) {
848
- final sizeMB = (progress.totalBytes / 1048576).toStringAsFixed(1);
849
- _log('gray', 'download ${sizeMB}MB $pctText');
920
+ final bucket = ((progress.fraction ?? 0) * 10).floor();
921
+ if (bucket != _lastLoggedPctBucket) {
922
+ _lastLoggedPctBucket = bucket;
923
+ final sizeMB = (progress.totalBytes / 1048576).toStringAsFixed(1);
924
+ _log('gray', 'download ${sizeMB}MB $pctText');
925
+ }
850
926
  }
851
927
 
852
928
  _state.value = _state.value.copyWith(
@@ -854,7 +930,6 @@ class OtaOverlayManager {
854
930
  fraction: progress.fraction,
855
931
  bytesReceived: progress.bytesReceived,
856
932
  totalBytes: progress.totalBytes > 0 ? progress.totalBytes : null,
857
- bytesPerSec: bytesPerSec,
858
933
  activeStep: newStep,
859
934
  );
860
935
  }
@@ -884,11 +959,16 @@ class OtaOverlayManager {
884
959
  final remaining = _minDuration - elapsed;
885
960
  final delay = remaining.isNegative ? Duration.zero : remaining;
886
961
 
962
+ // On success mark ALL steps complete + flag restarting, so the steps list
963
+ // finishes cleanly (every ✓, phase = "restarting") instead of freezing on
964
+ // whatever step the last native progress event happened to leave active.
887
965
  _state.value = _state.value.copyWith(
888
966
  hasError: hasError,
889
967
  errorText: errorText,
890
968
  canRetry: hasError,
891
- activeStep: hasError ? _state.value.activeStep : 4,
969
+ restarting: !hasError,
970
+ fraction: hasError ? null : 1.0,
971
+ activeStep: hasError ? _state.value.activeStep : _stepLabels.length,
892
972
  );
893
973
 
894
974
  final entry = _entry;
@@ -1,6 +1,6 @@
1
1
  name: flutter_ota_kit
2
2
  description: Open-source, self-hosted code push (OTA updates) for Flutter Android. Patches Dart AOT libapp.so and assets on cold start with integrity checks and crash rollback.
3
- version: 0.2.0
3
+ version: 0.2.2
4
4
  homepage: https://github.com/HYPER12755/flutter-ota-kit
5
5
  repository: https://github.com/HYPER12755/flutter-ota-kit
6
6
  issue_tracker: https://github.com/HYPER12755/flutter-ota-kit/issues
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_nazmiforreal/flutter-ota",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Command-line interface for flutter_ota_kit — an OTA code-push tool for Flutter, hot-updater compatible.",
5
5
  "bin": {
6
6
  "flutter-ota": "./bin/flutter-ota.js"