@_nazmiforreal/flutter-ota 0.1.28 → 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.
@@ -22,7 +22,7 @@ dependencies:
22
22
  # merged `flutter_ota_kit` package under lib/src/pkg/. The CLI depends on the
23
23
  # single package instead of eight. NOTE: `flutter_ota_kit` is a Flutter plugin,
24
24
  # so building the CLI now requires the Flutter SDK (not just the Dart SDK).
25
- flutter_ota_kit: ^0.2.1
25
+ flutter_ota_kit: ^0.2.2
26
26
  http: ^1.2.0
27
27
  shelf: ^1.4.0
28
28
 
@@ -613,14 +613,22 @@ internal class PatchManager(
613
613
  }
614
614
 
615
615
  private fun extractBaseAssetsIfNeeded(): Int {
616
- // Extract base APK assets to assets/0/ if not already done
616
+ // Store the base APK's flutter_assets as a single archive for rollback.
617
+ // We keep ONLY the compressed archive (not an uncompressed mirror dir):
618
+ // the previous implementation left both `assets/0/` (uncompressed) AND
619
+ // `assets/0/flutter_assets.apk` (zip) on disk — two full copies of every
620
+ // bundled asset. The uncompressed copy is never read (rollback re-extracts
621
+ // from the archive), so it was pure waste.
617
622
  if (!baseAssetsArchive.exists()) {
618
623
  baseAssetsArchive.parentFile?.mkdirs()
624
+ val tmp = File(assetsHistoryDir, "0.tmp")
619
625
  try {
620
- copyInstalledFlutterAssets(File(assetsHistoryDir, "0"))
621
- writeFlutterAssetsArchive(File(assetsHistoryDir, "0"), baseAssetsArchive)
626
+ copyInstalledFlutterAssets(tmp)
627
+ writeFlutterAssetsArchive(tmp, baseAssetsArchive)
622
628
  } catch (e: Exception) {
623
629
  Log.w(TAG, "failed to extract base assets", e)
630
+ } finally {
631
+ tmp.deleteRecursively()
624
632
  }
625
633
  }
626
634
  return 0
@@ -998,8 +1006,19 @@ internal class PatchManager(
998
1006
  meta.put("assetsRef", assetsRef)
999
1007
  archiveCurrentPatchToHistory(meta)
1000
1008
  } else {
1001
- // First patch ever - extract base assets
1002
- assetsRef = extractBaseAssetsIfNeeded()
1009
+ // First patch ever.
1010
+ //
1011
+ // Only extract the base APK's flutter_assets (as a rollback
1012
+ // reference) when this patch actually ships assets. A Dart-only
1013
+ // (code-only) patch never overlays assets, so extracting the
1014
+ // whole base asset tree here is pure waste — it was the main
1015
+ // cause of a first code-patch ballooning app storage by 100s of
1016
+ // MB. Code-only patches load assets straight from the APK.
1017
+ assetsRef = if (finalAssets != null) {
1018
+ extractBaseAssetsIfNeeded()
1019
+ } else {
1020
+ 0
1021
+ }
1003
1022
  meta.put("assetsRef", assetsRef)
1004
1023
  }
1005
1024
 
@@ -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 {
@@ -381,8 +381,9 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
381
381
  : '·';
382
382
 
383
383
  // 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.
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.
386
387
  final String status;
387
388
  if (failed) {
388
389
  status = 'error';
@@ -391,7 +392,7 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
391
392
  } else if (active) {
392
393
  final showPct = s.phase == PatchApplyPhase.downloading &&
393
394
  s.fraction != null;
394
- status = showPct ? _pct(s.fraction) : 'working $spinner';
395
+ status = showPct ? _pct(s.fraction) : 'working…';
395
396
  } else {
396
397
  status = 'wait';
397
398
  }
@@ -437,68 +438,53 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
437
438
 
438
439
  // ── PROGRESS ──────────────────────────────────────────────────────────
439
440
  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';
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
449
459
  }
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);
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;
463
464
 
464
465
  return _panelBox(
465
- child: Column(
466
- crossAxisAlignment: CrossAxisAlignment.start,
466
+ child: Row(
467
467
  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
- ),
492
- ),
493
- ],
468
+ Expanded(
469
+ child: _ProgressBar(
470
+ fraction: frac,
471
+ color: s.hasError ? _C.red : _C.blue,
472
+ ),
494
473
  ),
495
- const SizedBox(height: 12),
496
- Row(
497
- children: [
498
- _stat('speed', speed),
499
- _stat('eta', eta),
500
- _stat('phase', phase),
501
- ],
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
+ ),
502
488
  ),
503
489
  ],
504
490
  ),
@@ -605,22 +591,6 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
605
591
  child: child,
606
592
  );
607
593
 
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
- );
623
-
624
594
  Widget _dot(Color c) => Container(
625
595
  width: 11,
626
596
  height: 11,
@@ -644,19 +614,15 @@ class _OtaProgressOverlayState extends State<OtaProgressOverlay> {
644
614
  );
645
615
  }
646
616
 
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%.
617
+ /// A single-line determinate progress bar that never wraps.
654
618
  ///
655
619
  /// It's a real painted widget with a fixed height inside an [Expanded], so it
656
620
  /// always stays on one line regardless of width — unlike the old
657
- /// character-string bar, which wrapped when it didn't fit.
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%.
658
624
  class _ProgressBar extends StatefulWidget {
659
- final double? fraction;
625
+ final double fraction;
660
626
  final Color color;
661
627
 
662
628
  const _ProgressBar({required this.fraction, required this.color});
@@ -665,40 +631,22 @@ class _ProgressBar extends StatefulWidget {
665
631
  State<_ProgressBar> createState() => _ProgressBarState();
666
632
  }
667
633
 
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.
634
+ class _ProgressBarState extends State<_ProgressBar> {
635
+ // Smoothed value so the bar eases toward the target. Forward-only — it never
636
+ // visually goes backwards.
677
637
  double _shown = 0;
678
638
 
679
639
  @override
680
640
  void initState() {
681
641
  super.initState();
682
- _shown = widget.fraction ?? 0;
683
- if (widget.fraction == null) _indeterminate.repeat();
642
+ _shown = widget.fraction.clamp(0.0, 1.0);
684
643
  }
685
644
 
686
645
  @override
687
646
  void didUpdateWidget(covariant _ProgressBar old) {
688
647
  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();
648
+ final target = widget.fraction.clamp(0.0, 1.0);
649
+ if (target >= _shown) _shown = target;
702
650
  }
703
651
 
704
652
  static const _height = 8.0;
@@ -712,60 +660,24 @@ class _ProgressBarState extends State<_ProgressBar>
712
660
  height: _height,
713
661
  child: ColoredBox(
714
662
  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),
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
+ ),
764
676
  ),
765
677
  ),
766
678
  ),
767
- );
768
- },
679
+ ),
680
+ ),
769
681
  ),
770
682
  );
771
683
  }
@@ -869,10 +781,6 @@ class OtaOverlayManager {
869
781
  bool _disposed = false;
870
782
  DateTime? _startTime;
871
783
 
872
- // Download-speed tracking.
873
- int _lastBytes = 0;
874
- DateTime? _lastBytesAt;
875
-
876
784
  /// Last 10%-bucket we emitted a "download NN%" log line for, to throttle the
877
785
  /// feed. -1 = none yet.
878
786
  int _lastLoggedPctBucket = -1;
@@ -911,8 +819,6 @@ class OtaOverlayManager {
911
819
  if (overlay == null) return null;
912
820
 
913
821
  _startTime = DateTime.now();
914
- _lastBytes = 0;
915
- _lastBytesAt = null;
916
822
  _lastLoggedPctBucket = -1;
917
823
 
918
824
  final logs = <LogLine>[
@@ -984,30 +890,6 @@ class OtaOverlayManager {
984
890
  final phaseStep = _phaseToStep(progress.phase);
985
891
  final newStep = phaseStep < s.activeStep ? s.activeStep : phaseStep;
986
892
 
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
893
  // Log phase transitions once, with staggered detail lines.
1012
894
  if (s.phase != progress.phase) {
1013
895
  switch (progress.phase) {
@@ -1048,7 +930,6 @@ class OtaOverlayManager {
1048
930
  fraction: progress.fraction,
1049
931
  bytesReceived: progress.bytesReceived,
1050
932
  totalBytes: progress.totalBytes > 0 ? progress.totalBytes : null,
1051
- bytesPerSec: bytesPerSec,
1052
933
  activeStep: newStep,
1053
934
  );
1054
935
  }
@@ -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.1
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.28",
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"