feat: fix debug updater
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
+17
-1
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:kooltab2/views/settings_view.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../viewmodels/pin_lock_view_model.dart';
|
||||
@@ -11,6 +10,9 @@ import '../views/history_screen_view.dart';
|
||||
import '../views/pin_lock_view.dart';
|
||||
import '../views/product_form_view.dart';
|
||||
import '../views/product_list_view.dart';
|
||||
import '../views/settings_view.dart';
|
||||
import '../views/update_progress_view.dart';
|
||||
import '../utils/app_update_util.dart';
|
||||
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
|
||||
@@ -80,6 +82,20 @@ GoRouter createAppRouter(PinLockViewModel pinLockViewModel) {
|
||||
builder: (context, state) => const SettingsScreenView(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: '/update-progress',
|
||||
builder: (context, state) {
|
||||
final update = state.extra as UpdateInfo;
|
||||
final simulate = state.uri.queryParameters['simulate'] == 'true';
|
||||
final simulateError = state.uri.queryParameters['simulateError'] == 'true';
|
||||
return UpdateProgressView(
|
||||
update: update,
|
||||
simulate: simulate,
|
||||
simulateError: simulateError,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: '/dev',
|
||||
builder: (context, state) => const DevMenuView(),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:kooltab2/utils/app_update_util.dart';
|
||||
import 'package:ota_update/ota_update.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'app_config.dart';
|
||||
|
||||
@@ -43,7 +42,7 @@ class UpdateChecker {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: !update.mandatory,
|
||||
builder: (_) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text("Update available"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -57,14 +56,13 @@ class UpdateChecker {
|
||||
actions: [
|
||||
if (!update.mandatory)
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text("Later"),
|
||||
),
|
||||
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await _downloadAndInstall(update);
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
context.push('/update-progress', extra: update);
|
||||
},
|
||||
child: const Text("Update"),
|
||||
),
|
||||
@@ -73,45 +71,3 @@ class UpdateChecker {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadAndInstall(UpdateInfo update) async {
|
||||
try {
|
||||
final url = update.download.startsWith("http")
|
||||
? update.download
|
||||
: "$kUpdateServerUrl${update.download}";
|
||||
OtaUpdate()
|
||||
.execute(
|
||||
url,
|
||||
destinationFilename: "update.apk",
|
||||
sha256checksum: update.sha256,
|
||||
)
|
||||
.listen((OtaEvent event) {
|
||||
debugPrint("OTA status: ${event.status}");
|
||||
debugPrint("OTA value: ${event.value}");
|
||||
|
||||
switch (event.status) {
|
||||
case OtaStatus.DOWNLOADING:
|
||||
final progress = double.tryParse(event.value ?? "0") ?? 0;
|
||||
debugPrint("Downloading ${progress.toStringAsFixed(0)}%");
|
||||
break;
|
||||
case OtaStatus.INSTALLING:
|
||||
debugPrint("Installing update");
|
||||
break;
|
||||
case OtaStatus.INSTALLATION_ERROR:
|
||||
debugPrint("Installation error: ${event.value}");
|
||||
break;
|
||||
case OtaStatus.DOWNLOAD_ERROR:
|
||||
debugPrint("Download error: ${event.value}");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, onError: (e, stack) {
|
||||
debugPrint("OTA stream error: $e");
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
});
|
||||
} catch (e, stack) {
|
||||
debugPrint("OTA update failed: $e");
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:ota_update/ota_update.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../utils/app_config.dart';
|
||||
import '../utils/app_update_util.dart';
|
||||
|
||||
enum OtaPhase { downloading, installing, done, error }
|
||||
|
||||
class UpdateProgressViewModel extends ChangeNotifier {
|
||||
final UpdateInfo update;
|
||||
final bool simulate;
|
||||
final bool simulateError;
|
||||
|
||||
OtaPhase _phase = OtaPhase.downloading;
|
||||
double _progress = 0;
|
||||
String _statusText = 'Preparing download…';
|
||||
String? _errorMessage;
|
||||
bool _done = false;
|
||||
|
||||
StreamSubscription? _subscription;
|
||||
Timer? _simTimer;
|
||||
|
||||
OtaPhase get phase => _phase;
|
||||
double get progress => _progress;
|
||||
String get statusText => _statusText;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
UpdateProgressViewModel({
|
||||
required this.update,
|
||||
this.simulate = false,
|
||||
this.simulateError = false,
|
||||
});
|
||||
|
||||
Future<void> start() async {
|
||||
if (simulate) {
|
||||
_runSimulation();
|
||||
return;
|
||||
}
|
||||
|
||||
final url = update.download.startsWith('http')
|
||||
? update.download
|
||||
: '$kUpdateServerUrl${update.download}';
|
||||
|
||||
debugPrint('UpdateProgressViewModel: starting download from $url');
|
||||
|
||||
_subscription = OtaUpdate()
|
||||
.execute(
|
||||
url,
|
||||
destinationFilename: 'update.apk',
|
||||
sha256checksum: update.sha256,
|
||||
)
|
||||
.listen(
|
||||
_onEvent,
|
||||
onError: _onError,
|
||||
onDone: _onDone,
|
||||
);
|
||||
}
|
||||
|
||||
void _runSimulation() {
|
||||
debugPrint('UpdateProgressViewModel: running simulated download');
|
||||
|
||||
int step = 0;
|
||||
const totalSteps = 20;
|
||||
const stepDuration = Duration(milliseconds: 500);
|
||||
|
||||
_simTimer = Timer.periodic(stepDuration, (timer) {
|
||||
step++;
|
||||
|
||||
if (simulateError && step == 10) {
|
||||
timer.cancel();
|
||||
_phase = OtaPhase.error;
|
||||
_errorMessage = 'Simulated download error (network timeout)';
|
||||
_statusText = 'Download failed';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step < totalSteps) {
|
||||
_progress = step / totalSteps;
|
||||
_statusText = 'Downloading ${(_progress * 100).toStringAsFixed(0)}%';
|
||||
_phase = OtaPhase.downloading;
|
||||
notifyListeners();
|
||||
} else {
|
||||
timer.cancel();
|
||||
_startInstallPhase();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startInstallPhase() {
|
||||
_phase = OtaPhase.installing;
|
||||
_progress = 1.0;
|
||||
_statusText = 'Installing update…';
|
||||
notifyListeners();
|
||||
|
||||
_simTimer = Timer(const Duration(seconds: 3), () {
|
||||
_phase = OtaPhase.done;
|
||||
_progress = 1.0;
|
||||
_statusText = 'Update installed! Restarting…';
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
if (_done) return;
|
||||
_done = true;
|
||||
_phase = OtaPhase.done;
|
||||
_progress = 1.0;
|
||||
_statusText = 'Update installed! Restarting…';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onEvent(OtaEvent event) {
|
||||
switch (event.status) {
|
||||
case OtaStatus.DOWNLOADING:
|
||||
final raw = double.tryParse(event.value ?? '0') ?? 0;
|
||||
_progress = (raw / 100).clamp(0.0, 1.0);
|
||||
_statusText = 'Downloading ${_progress.toStringAsFixed(0)}%';
|
||||
_phase = OtaPhase.downloading;
|
||||
notifyListeners();
|
||||
|
||||
case OtaStatus.INSTALLING:
|
||||
_phase = OtaPhase.installing;
|
||||
_progress = 1.0;
|
||||
_statusText = 'Installing update…';
|
||||
notifyListeners();
|
||||
|
||||
case OtaStatus.INSTALLATION_DONE:
|
||||
_onDone();
|
||||
return;
|
||||
|
||||
case OtaStatus.DOWNLOAD_ERROR:
|
||||
_phase = OtaPhase.error;
|
||||
_errorMessage = 'Download failed: ${event.value}';
|
||||
_statusText = 'Download failed';
|
||||
Sentry.captureMessage('OTA download error: ${event.value}');
|
||||
notifyListeners();
|
||||
|
||||
case OtaStatus.INSTALLATION_ERROR:
|
||||
_phase = OtaPhase.error;
|
||||
_errorMessage = 'Installation failed: ${event.value}';
|
||||
_statusText = 'Installation failed';
|
||||
Sentry.captureMessage('OTA installation error: ${event.value}');
|
||||
notifyListeners();
|
||||
|
||||
case OtaStatus.ALREADY_RUNNING_ERROR:
|
||||
_phase = OtaPhase.error;
|
||||
_errorMessage = 'Update already in progress';
|
||||
_statusText = 'Update already running';
|
||||
notifyListeners();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onError(Object error, StackTrace stack) {
|
||||
debugPrint('UpdateProgressViewModel: stream error: $error');
|
||||
_phase = OtaPhase.error;
|
||||
_errorMessage = error.toString();
|
||||
_statusText = 'Update failed';
|
||||
Sentry.captureException(error, stackTrace: stack);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_simTimer?.cancel();
|
||||
_simTimer = null;
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
+106
-17
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../utils/app_update_util.dart';
|
||||
import '../viewmodels/dev_menu_view_model.dart';
|
||||
|
||||
class DevMenuView extends StatefulWidget {
|
||||
@@ -128,9 +129,9 @@ class _DevMenuViewState extends State<DevMenuView> {
|
||||
children: [
|
||||
_Tile(
|
||||
icon: Icons.update_rounded,
|
||||
title: 'Mock update available',
|
||||
subtitle: 'Simulate OTA update dialog',
|
||||
onTap: () => _showMockUpdateDialog(),
|
||||
title: 'Simulate update download',
|
||||
subtitle: 'Open the download progress screen',
|
||||
onTap: () => _showSimulateUpdateDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -209,28 +210,66 @@ class _DevMenuViewState extends State<DevMenuView> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showMockUpdateDialog() {
|
||||
void _showSimulateUpdateDialog() {
|
||||
final fakeUpdate = UpdateInfo(
|
||||
update: true,
|
||||
version: '99.0.0',
|
||||
notes: 'Bug fixes and performance improvements.',
|
||||
mandatory: false,
|
||||
sha256: 'abc123',
|
||||
download: 'https://example.com/fake-update.apk',
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Update available'),
|
||||
content: const Column(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Simulate update'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Version 99.0.0 is available.'),
|
||||
SizedBox(height: 12),
|
||||
Text('Bug fixes and performance improvements.'),
|
||||
const Text('Choose a simulation mode:'),
|
||||
const SizedBox(height: 16),
|
||||
_SimOption(
|
||||
icon: Icons.download_rounded,
|
||||
label: 'Successful download',
|
||||
description: 'Progress 0→100%, then install, then done',
|
||||
onTap: () {
|
||||
Navigator.pop(dialogContext);
|
||||
context.push(
|
||||
'/update-progress?simulate=true',
|
||||
extra: fakeUpdate,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SimOption(
|
||||
icon: Icons.error_outline_rounded,
|
||||
label: 'Download error',
|
||||
description: 'Fails at 50% with a network timeout',
|
||||
onTap: () {
|
||||
Navigator.pop(dialogContext);
|
||||
context.push(
|
||||
'/update-progress?simulate=true&simulateError=true',
|
||||
extra: fakeUpdate,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SimOption(
|
||||
icon: Icons.wifi_off_rounded,
|
||||
label: 'Real download (will fail)',
|
||||
description: 'Attempts real OTA with fake URL',
|
||||
onTap: () {
|
||||
Navigator.pop(dialogContext);
|
||||
context.push('/update-progress', extra: fakeUpdate);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Later'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Update'),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -238,6 +277,56 @@ class _DevMenuViewState extends State<DevMenuView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SimOption extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String description;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SimOption({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.titleSmall),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
@@ -344,7 +344,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: !update.mandatory,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text("Update available"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -358,30 +358,13 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
|
||||
actions: [
|
||||
if (!update.mandatory)
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text("Later"),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
try {
|
||||
await context.read<SettingsViewModel>().installUpdate(
|
||||
update,
|
||||
onProgress: (progress) {
|
||||
debugPrint(
|
||||
"Download ${(progress * 100).toStringAsFixed(0)}%",
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e, stack) {
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Update failed: $e")));
|
||||
}
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
context.push('/update-progress', extra: update);
|
||||
},
|
||||
child: const Text("Update"),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../utils/app_update_util.dart';
|
||||
import '../viewmodels/update_progress_view_model.dart';
|
||||
|
||||
class UpdateProgressView extends StatefulWidget {
|
||||
final UpdateInfo update;
|
||||
final bool simulate;
|
||||
final bool simulateError;
|
||||
|
||||
const UpdateProgressView({
|
||||
super.key,
|
||||
required this.update,
|
||||
this.simulate = false,
|
||||
this.simulateError = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpdateProgressView> createState() => _UpdateProgressViewState();
|
||||
}
|
||||
|
||||
class _UpdateProgressViewState extends State<UpdateProgressView> {
|
||||
late final UpdateProgressViewModel _vm;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_vm = UpdateProgressViewModel(
|
||||
update: widget.update,
|
||||
simulate: widget.simulate,
|
||||
simulateError: widget.simulateError,
|
||||
);
|
||||
_vm.addListener(_onVmChange);
|
||||
_vm.start();
|
||||
}
|
||||
|
||||
void _onVmChange() {
|
||||
if (!mounted) return;
|
||||
|
||||
if (_vm.phase == OtaPhase.done) {
|
||||
_showRestartDialog();
|
||||
} else if (_vm.phase == OtaPhase.error) {
|
||||
_showErrorSnackBar();
|
||||
}
|
||||
}
|
||||
|
||||
void _showRestartDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Update ready'),
|
||||
content: const Text(
|
||||
'The update has been downloaded and installed. '
|
||||
'Restart the app now to apply the changes.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
this.context.go('/bar');
|
||||
},
|
||||
child: const Text('Later'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Restart app'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorSnackBar() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_vm.errorMessage ?? 'Update failed'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
action: SnackBarAction(
|
||||
label: 'Retry',
|
||||
onPressed: () {
|
||||
_vm.cancel();
|
||||
_vm.start();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vm.removeListener(_onVmChange);
|
||||
_vm.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => _showCancelDialog(),
|
||||
),
|
||||
title: const Text('Updating'),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: _vm,
|
||||
builder: (context, child) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
_buildProgress(_vm),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatus(_vm),
|
||||
const Spacer(),
|
||||
_buildVersionInfo(_vm),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.system_update_alt_rounded,
|
||||
size: 56,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'KoolTab',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Version ${widget.update.version}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgress(UpdateProgressViewModel vm) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: CircularProgressIndicator(
|
||||
value: vm.phase == OtaPhase.installing ? null : vm.progress,
|
||||
strokeWidth: 10,
|
||||
backgroundColor: scheme.primary.withValues(alpha: 0.12),
|
||||
color: vm.phase == OtaPhase.error
|
||||
? scheme.error
|
||||
: scheme.primary,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (vm.phase == OtaPhase.error)
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 48,
|
||||
color: scheme.error,
|
||||
)
|
||||
else if (vm.phase == OtaPhase.installing)
|
||||
Icon(
|
||||
Icons.settings_rounded,
|
||||
size: 48,
|
||||
color: scheme.primary,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'${(vm.progress * 100).toStringAsFixed(0)}%',
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
vm.phase == OtaPhase.installing ? 'Installing' : '',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (vm.phase == OtaPhase.downloading)
|
||||
LinearProgressIndicator(
|
||||
value: vm.progress,
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatus(UpdateProgressViewModel vm) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
final color = switch (vm.phase) {
|
||||
OtaPhase.error => scheme.error,
|
||||
OtaPhase.done => Colors.green,
|
||||
_ => scheme.onSurface,
|
||||
};
|
||||
|
||||
return Text(
|
||||
vm.statusText,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVersionInfo(UpdateProgressViewModel vm) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Do not close the app during the update',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showCancelDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Cancel update?'),
|
||||
content: const Text(
|
||||
'The update is in progress. If you leave now, '
|
||||
'the app may become unstable.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Continue update'),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_vm.cancel();
|
||||
this.context.go('/bar');
|
||||
},
|
||||
child: const Text('Cancel update'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.3+1
|
||||
version: 1.0.7+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
Reference in New Issue
Block a user