feat: auto-updater
This commit is contained in:
@@ -20,6 +20,8 @@ android {
|
|||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
@@ -60,3 +62,7 @@ kotlin {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="kooltab2"
|
android:label="kooltab2"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.ota_update_provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/ota_update_paths" />
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
<receiver android:name="sk.fourq.otaupdate.InstallResultReceiver" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.ACTION_INSTALL_COMPLETE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<files-path
|
||||||
|
name="ota_update"
|
||||||
|
path="." />
|
||||||
|
|
||||||
|
</paths>
|
||||||
@@ -13,107 +13,6 @@ class KoolTabApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _KoolTabAppState extends State<KoolTabApp> {
|
class _KoolTabAppState extends State<KoolTabApp> {
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_checkForUpdates();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _checkForUpdates() async {
|
|
||||||
final updater = AppUpdateUtil(serverUrl: "http://localhost:3000");
|
|
||||||
|
|
||||||
try {
|
|
||||||
final update = await updater.checkForUpdate();
|
|
||||||
|
|
||||||
if (update == null) {
|
|
||||||
debugPrint("No update available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint("Update available: ${update.version}");
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
_showUpdateDialog(update);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Update check failed: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showUpdateDialog(UpdateInfo update) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: !update.mandatory,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text("Update available"),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Version ${update.version} is available.",
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(update.notes),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
if (!update.mandatory)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Text("Later"),
|
|
||||||
),
|
|
||||||
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
await _downloadAndInstall(update);
|
|
||||||
},
|
|
||||||
child: const Text("Update"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _downloadAndInstall(UpdateInfo update) async {
|
|
||||||
final updater = AppUpdateUtil(
|
|
||||||
serverUrl: "http://localhost:3000",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final apk = await updater.downloadApk(
|
|
||||||
update,
|
|
||||||
onProgress: (progress) {
|
|
||||||
debugPrint(
|
|
||||||
"Download ${(progress * 100).toStringAsFixed(0)}%",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final valid = await updater.verifySha256(
|
|
||||||
apk,
|
|
||||||
update.sha256,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw Exception("Invalid update file");
|
|
||||||
}
|
|
||||||
|
|
||||||
await updater.installApk(apk);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Update failed: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class AppUpdateUtil {
|
|||||||
|
|
||||||
final response = await http.get(url);
|
final response = await http.get(url);
|
||||||
|
|
||||||
|
debugPrint("Response from update server: ${response.body}");
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Update server unavailable");
|
throw Exception("Update server unavailable");
|
||||||
}
|
}
|
||||||
@@ -71,96 +73,100 @@ class AppUpdateUtil {
|
|||||||
return UpdateInfo.fromJson(data);
|
return UpdateInfo.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads the APK
|
// /// Downloads the APK
|
||||||
Future<File> downloadApk(
|
// Future<File> downloadApk(
|
||||||
UpdateInfo update, {
|
// UpdateInfo update, {
|
||||||
|
//
|
||||||
Function(double progress)? onProgress,
|
// Function(double progress)? onProgress,
|
||||||
}) async {
|
// }) async {
|
||||||
final url = update.download.startsWith("http")
|
// final url = update.download.startsWith("http")
|
||||||
? update.download
|
// ? update.download
|
||||||
: "$serverUrl${update.download}";
|
// : "$serverUrl${update.download}";
|
||||||
|
//
|
||||||
final request = http.Request("GET", Uri.parse(url));
|
// final request = http.Request("GET", Uri.parse(url));
|
||||||
|
//
|
||||||
final response = await request.send();
|
// final response = await request.send();
|
||||||
|
//
|
||||||
if (response.statusCode != 200) {
|
// if (response.statusCode != 200) {
|
||||||
throw Exception("APK download failed");
|
// throw Exception("APK download failed");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
final total = response.contentLength ?? 0;
|
// final total = response.contentLength ?? 0;
|
||||||
|
//
|
||||||
int received = 0;
|
// int received = 0;
|
||||||
|
//
|
||||||
final directory = await getTemporaryDirectory();
|
// final directory = await getTemporaryDirectory();
|
||||||
|
//
|
||||||
final file = File("${directory.path}/update.apk");
|
// final file = File("${directory.path}/update.apk");
|
||||||
|
//
|
||||||
final sink = file.openWrite();
|
// final sink = file.openWrite();
|
||||||
|
//
|
||||||
await for (final chunk in response.stream) {
|
// await for (final chunk in response.stream) {
|
||||||
sink.add(chunk);
|
// sink.add(chunk);
|
||||||
|
//
|
||||||
received += chunk.length;
|
// received += chunk.length;
|
||||||
|
//
|
||||||
if (total > 0 && onProgress != null) {
|
// if (total > 0 && onProgress != null) {
|
||||||
onProgress(received / total);
|
// onProgress(received / total);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
await sink.close();
|
// await sink.close();
|
||||||
|
//
|
||||||
return file;
|
// return file;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/// Verifies APK checksum
|
// /// Verifies APK checksum
|
||||||
Future<bool> verifySha256(File file, String expectedHash) async {
|
// Future<bool> verifySha256(File file, String expectedHash) async {
|
||||||
final bytes = await file.readAsBytes();
|
// final bytes = await file.readAsBytes();
|
||||||
|
//
|
||||||
final digest = sha256.convert(bytes);
|
// final digest = sha256.convert(bytes);
|
||||||
|
//
|
||||||
return digest.toString().toLowerCase() == expectedHash.toLowerCase();
|
// return digest.toString().toLowerCase() == expectedHash.toLowerCase();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/// Opens Android APK installer
|
// /// Opens Android APK installer
|
||||||
Future<void> installApk(File file) async {
|
// Future<void> installApk(File file) async {
|
||||||
// final result =
|
// final result =
|
||||||
// await OpenFilex.open(
|
// await OpenFilex.open(
|
||||||
// file.path,
|
// file.path,
|
||||||
// );
|
// );
|
||||||
//
|
//
|
||||||
//
|
// if(result.type == ResultType.done) {
|
||||||
// if (result.type != ResultType.done) {
|
// debugPrint("APK installer opened successfully");
|
||||||
// throw Exception(
|
// }
|
||||||
// "Could not open APK installer",
|
//
|
||||||
// );
|
//
|
||||||
// }
|
// if (result.type != ResultType.done) {
|
||||||
}
|
// throw Exception(
|
||||||
|
// "Could not open APK installer",
|
||||||
/// Full update flow
|
// );
|
||||||
///
|
// }
|
||||||
/// Returns:
|
// }
|
||||||
/// - null if no update exists
|
//
|
||||||
/// - UpdateInfo if update is available
|
// /// Full update flow
|
||||||
///
|
// ///
|
||||||
Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
|
// /// Returns:
|
||||||
final info = await checkForUpdate();
|
// /// - null if no update exists
|
||||||
|
// /// - UpdateInfo if update is available
|
||||||
if (info == null) {
|
// ///
|
||||||
return null;
|
// Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
|
||||||
}
|
// final info = await checkForUpdate();
|
||||||
|
//
|
||||||
final apk = await downloadApk(info, onProgress: onProgress);
|
// if (info == null) {
|
||||||
|
// return null;
|
||||||
final valid = await verifySha256(apk, info.sha256);
|
// }
|
||||||
|
//
|
||||||
if (!valid) {
|
// final apk = await downloadApk(info, onProgress: onProgress);
|
||||||
throw Exception("APK checksum mismatch");
|
//
|
||||||
}
|
// final valid = await verifySha256(apk, info.sha256);
|
||||||
|
//
|
||||||
await installApk(apk);
|
// if (!valid) {
|
||||||
|
// throw Exception("APK checksum mismatch");
|
||||||
return info;
|
// }
|
||||||
}
|
//
|
||||||
|
// await installApk(apk);
|
||||||
|
//
|
||||||
|
// return info;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kooltab2/utils/app_update_util.dart';
|
||||||
|
import 'package:ota_update/ota_update.dart';
|
||||||
|
|
||||||
|
class UpdateChecker {
|
||||||
|
static bool _hasChecked = false;
|
||||||
|
|
||||||
|
static Future<void> check(BuildContext context) async {
|
||||||
|
if (_hasChecked) {
|
||||||
|
debugPrint("Update already checked this boot");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasChecked = true;
|
||||||
|
|
||||||
|
final updater = AppUpdateUtil(
|
||||||
|
serverUrl: "https://updater.brammie15.dev",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final update = await updater.checkForUpdate();
|
||||||
|
|
||||||
|
if (update == null) {
|
||||||
|
debugPrint("No update available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Update available: ${update.version}");
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
_showUpdateDialog(context, update);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Update check failed: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _showUpdateDialog(
|
||||||
|
BuildContext context,
|
||||||
|
UpdateInfo update,
|
||||||
|
) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: !update.mandatory,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text("Update available"),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Version ${update.version} is available."),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(update.notes),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (!update.mandatory)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Later"),
|
||||||
|
),
|
||||||
|
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await _downloadAndInstall(update);
|
||||||
|
},
|
||||||
|
child: const Text("Update"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadAndInstall(UpdateInfo update) async {
|
||||||
|
try {
|
||||||
|
final url = update.download.startsWith("http")
|
||||||
|
? update.download
|
||||||
|
: "https://updater.brammie15.dev${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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
"OTA update failed: $e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:kooltab2/utils/app_update_util.dart';
|
||||||
|
import 'package:ota_update/ota_update.dart';
|
||||||
|
|
||||||
import '../models/settings.dart';
|
import '../models/settings.dart';
|
||||||
import '../services/settings_service.dart';
|
import '../services/settings_service.dart';
|
||||||
@@ -13,6 +15,14 @@ class SettingsViewModel extends ChangeNotifier {
|
|||||||
bool _hasLoaded = false;
|
bool _hasLoaded = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
|
final AppUpdateUtil _updater = AppUpdateUtil(
|
||||||
|
serverUrl: "https://updater.brammie15.dev",
|
||||||
|
);
|
||||||
|
|
||||||
|
bool _checkingForUpdates = false;
|
||||||
|
|
||||||
|
bool get checkingForUpdates => _checkingForUpdates;
|
||||||
|
|
||||||
AppSettings get settings => _settings;
|
AppSettings get settings => _settings;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
bool get hasLoaded => _hasLoaded;
|
bool get hasLoaded => _hasLoaded;
|
||||||
@@ -41,6 +51,64 @@ class SettingsViewModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UpdateInfo?> checkForUpdates() async {
|
||||||
|
if (_checkingForUpdates) return null;
|
||||||
|
|
||||||
|
_checkingForUpdates = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await _updater.checkForUpdate();
|
||||||
|
} finally {
|
||||||
|
_checkingForUpdates = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> installUpdate(
|
||||||
|
UpdateInfo update, {
|
||||||
|
void Function(double progress)? onProgress,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
final url = update.download.startsWith("http")
|
||||||
|
? update.download
|
||||||
|
: "https://updater.brammie15.dev${update.download}";
|
||||||
|
|
||||||
|
debugPrint("Download Url: ${url}");
|
||||||
|
|
||||||
|
final stream = OtaUpdate().execute(
|
||||||
|
url,
|
||||||
|
destinationFilename: "update.apk",
|
||||||
|
sha256checksum: update.sha256,
|
||||||
|
);
|
||||||
|
|
||||||
|
await for (final event in stream) {
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"OTA: ${event.status} ${event.value}",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event.status == OtaStatus.DOWNLOADING) {
|
||||||
|
final progress =
|
||||||
|
double.tryParse(event.value ?? "0") ?? 0;
|
||||||
|
|
||||||
|
onProgress?.call(progress / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.status == OtaStatus.DOWNLOAD_ERROR) {
|
||||||
|
throw Exception(
|
||||||
|
"Download failed: ${event.value}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.status == OtaStatus.INSTALLATION_ERROR) {
|
||||||
|
throw Exception(
|
||||||
|
"Installation failed: ${event.value}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Just flips the preference flag. Caller is responsible for having
|
/// Just flips the preference flag. Caller is responsible for having
|
||||||
/// already set/verified the actual PIN via PinLockViewModel first.
|
/// already set/verified the actual PIN via PinLockViewModel first.
|
||||||
Future<void> updatePinRequired(bool value) =>
|
Future<void> updatePinRequired(bool value) =>
|
||||||
|
|||||||
@@ -11,11 +11,26 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
|||||||
import '../models/bar_tab.dart';
|
import '../models/bar_tab.dart';
|
||||||
import '../models/product.dart';
|
import '../models/product.dart';
|
||||||
import '../models/tab_item.dart';
|
import '../models/tab_item.dart';
|
||||||
|
import '../utils/app_updater.dart';
|
||||||
import '../viewmodels/bar_screen_view_model.dart';
|
import '../viewmodels/bar_screen_view_model.dart';
|
||||||
|
|
||||||
class BarScreenView extends StatelessWidget {
|
class BarScreenView extends StatefulWidget {
|
||||||
const BarScreenView({super.key});
|
const BarScreenView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BarScreenView> createState() => _BarScreenViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BarScreenViewState extends State<BarScreenView> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
UpdateChecker.check(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final viewModel = context.watch<BarScreenViewModel>();
|
final viewModel = context.watch<BarScreenViewModel>();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../models/settings.dart';
|
import '../models/settings.dart';
|
||||||
|
import '../utils/app_update_util.dart';
|
||||||
import '../viewmodels/pin_lock_view_model.dart';
|
import '../viewmodels/pin_lock_view_model.dart';
|
||||||
import '../viewmodels/settings_view_model.dart';
|
import '../viewmodels/settings_view_model.dart';
|
||||||
|
|
||||||
@@ -213,12 +214,102 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
_SettingsSection(
|
||||||
|
title: 'Updates',
|
||||||
|
children: [
|
||||||
|
_SettingsTile(
|
||||||
|
icon: Icons.system_update_alt_rounded,
|
||||||
|
title: settingsViewModel.checkingForUpdates
|
||||||
|
? 'Checking for updates...'
|
||||||
|
: 'Check for updates',
|
||||||
|
onTap: settingsViewModel.checkingForUpdates
|
||||||
|
? null
|
||||||
|
: _checkForUpdates,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkForUpdates() async {
|
||||||
|
final vm = context.read<SettingsViewModel>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final update = await vm.checkForUpdates();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (update == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('You are already on the latest version.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showUpdateDialog(update);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Update check failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUpdateDialog(UpdateInfo update) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: !update.mandatory,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text("Update available"),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Version ${update.version} is available."),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(update.notes),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (!update.mandatory)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
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) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Update failed: $e")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Update"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsSection extends StatelessWidget {
|
class _SettingsSection extends StatelessWidget {
|
||||||
|
|||||||
@@ -632,6 +632,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.0"
|
||||||
|
ota_update:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: ota_update
|
||||||
|
sha256: "1f4c7c3c4f306729a6c00b84435096ce2d8b28439013f7237173acc699b2abc8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+2
-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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.1+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
@@ -49,6 +49,7 @@ dependencies:
|
|||||||
http: ^1.6.0
|
http: ^1.6.0
|
||||||
package_info_plus: ^10.1.0
|
package_info_plus: ^10.1.0
|
||||||
open_filex: ^4.7.0
|
open_filex: ^4.7.0
|
||||||
|
ota_update: ^7.1.0
|
||||||
# permission_handler: ^12.0.3
|
# permission_handler: ^12.0.3
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user