@_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.
- package/README.md +112 -18
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/README.md +146 -2
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +36 -8
- package/dart-src/packages/cli-tools/pubspec.yaml +1 -1
- package/dart-src/root/README.md +193 -253
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +24 -5
- package/dart-src/root/lib/src/ota_progress_overlay.dart +176 -96
- package/dart-src/root/pubspec.yaml +1 -1
- package/package.json +1 -1
|
@@ -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 (
|
|
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 <
|
|
349
|
-
final active = i == s.
|
|
350
|
-
final failed = i ==
|
|
351
|
-
final pending = i >
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (s.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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:
|
|
439
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
466
|
+
child: Row(
|
|
440
467
|
children: [
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
779
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
849
|
-
|
|
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
|
-
|
|
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.
|
|
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