feat: auto-updater
This commit is contained in:
@@ -20,6 +20,8 @@ android {
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
@@ -60,3 +62,7 @@ kotlin {
|
||||
flutter {
|
||||
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">
|
||||
|
||||
<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
|
||||
android:label="kooltab2"
|
||||
android:name="${applicationName}"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
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> {
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -58,6 +58,8 @@ class AppUpdateUtil {
|
||||
|
||||
final response = await http.get(url);
|
||||
|
||||
debugPrint("Response from update server: ${response.body}");
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Update server unavailable");
|
||||
}
|
||||
@@ -71,96 +73,100 @@ class AppUpdateUtil {
|
||||
return UpdateInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// Downloads the APK
|
||||
Future<File> downloadApk(
|
||||
UpdateInfo update, {
|
||||
|
||||
Function(double progress)? onProgress,
|
||||
}) async {
|
||||
final url = update.download.startsWith("http")
|
||||
? update.download
|
||||
: "$serverUrl${update.download}";
|
||||
|
||||
final request = http.Request("GET", Uri.parse(url));
|
||||
|
||||
final response = await request.send();
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("APK download failed");
|
||||
}
|
||||
|
||||
final total = response.contentLength ?? 0;
|
||||
|
||||
int received = 0;
|
||||
|
||||
final directory = await getTemporaryDirectory();
|
||||
|
||||
final file = File("${directory.path}/update.apk");
|
||||
|
||||
final sink = file.openWrite();
|
||||
|
||||
await for (final chunk in response.stream) {
|
||||
sink.add(chunk);
|
||||
|
||||
received += chunk.length;
|
||||
|
||||
if (total > 0 && onProgress != null) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
}
|
||||
|
||||
await sink.close();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/// Verifies APK checksum
|
||||
Future<bool> verifySha256(File file, String expectedHash) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
final digest = sha256.convert(bytes);
|
||||
|
||||
return digest.toString().toLowerCase() == expectedHash.toLowerCase();
|
||||
}
|
||||
|
||||
/// Opens Android APK installer
|
||||
Future<void> installApk(File file) async {
|
||||
// /// Downloads the APK
|
||||
// Future<File> downloadApk(
|
||||
// UpdateInfo update, {
|
||||
//
|
||||
// Function(double progress)? onProgress,
|
||||
// }) async {
|
||||
// final url = update.download.startsWith("http")
|
||||
// ? update.download
|
||||
// : "$serverUrl${update.download}";
|
||||
//
|
||||
// final request = http.Request("GET", Uri.parse(url));
|
||||
//
|
||||
// final response = await request.send();
|
||||
//
|
||||
// if (response.statusCode != 200) {
|
||||
// throw Exception("APK download failed");
|
||||
// }
|
||||
//
|
||||
// final total = response.contentLength ?? 0;
|
||||
//
|
||||
// int received = 0;
|
||||
//
|
||||
// final directory = await getTemporaryDirectory();
|
||||
//
|
||||
// final file = File("${directory.path}/update.apk");
|
||||
//
|
||||
// final sink = file.openWrite();
|
||||
//
|
||||
// await for (final chunk in response.stream) {
|
||||
// sink.add(chunk);
|
||||
//
|
||||
// received += chunk.length;
|
||||
//
|
||||
// if (total > 0 && onProgress != null) {
|
||||
// onProgress(received / total);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// await sink.close();
|
||||
//
|
||||
// return file;
|
||||
// }
|
||||
//
|
||||
// /// Verifies APK checksum
|
||||
// Future<bool> verifySha256(File file, String expectedHash) async {
|
||||
// final bytes = await file.readAsBytes();
|
||||
//
|
||||
// final digest = sha256.convert(bytes);
|
||||
//
|
||||
// return digest.toString().toLowerCase() == expectedHash.toLowerCase();
|
||||
// }
|
||||
//
|
||||
// /// Opens Android APK installer
|
||||
// Future<void> installApk(File file) async {
|
||||
// final result =
|
||||
// await OpenFilex.open(
|
||||
// file.path,
|
||||
// );
|
||||
//
|
||||
// if(result.type == ResultType.done) {
|
||||
// debugPrint("APK installer opened successfully");
|
||||
// }
|
||||
//
|
||||
//
|
||||
// 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
|
||||
///
|
||||
Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
|
||||
final info = await checkForUpdate();
|
||||
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final apk = await downloadApk(info, onProgress: onProgress);
|
||||
|
||||
final valid = await verifySha256(apk, info.sha256);
|
||||
|
||||
if (!valid) {
|
||||
throw Exception("APK checksum mismatch");
|
||||
}
|
||||
|
||||
await installApk(apk);
|
||||
|
||||
return info;
|
||||
}
|
||||
// }
|
||||
//
|
||||
// /// Full update flow
|
||||
// ///
|
||||
// /// Returns:
|
||||
// /// - null if no update exists
|
||||
// /// - UpdateInfo if update is available
|
||||
// ///
|
||||
// Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
|
||||
// final info = await checkForUpdate();
|
||||
//
|
||||
// if (info == null) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// final apk = await downloadApk(info, onProgress: onProgress);
|
||||
//
|
||||
// final valid = await verifySha256(apk, info.sha256);
|
||||
//
|
||||
// if (!valid) {
|
||||
// throw Exception("APK checksum mismatch");
|
||||
// }
|
||||
//
|
||||
// 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:kooltab2/utils/app_update_util.dart';
|
||||
import 'package:ota_update/ota_update.dart';
|
||||
|
||||
import '../models/settings.dart';
|
||||
import '../services/settings_service.dart';
|
||||
@@ -13,6 +15,14 @@ class SettingsViewModel extends ChangeNotifier {
|
||||
bool _hasLoaded = false;
|
||||
String? _errorMessage;
|
||||
|
||||
final AppUpdateUtil _updater = AppUpdateUtil(
|
||||
serverUrl: "https://updater.brammie15.dev",
|
||||
);
|
||||
|
||||
bool _checkingForUpdates = false;
|
||||
|
||||
bool get checkingForUpdates => _checkingForUpdates;
|
||||
|
||||
AppSettings get settings => _settings;
|
||||
bool get isLoading => _isLoading;
|
||||
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
|
||||
/// already set/verified the actual PIN via PinLockViewModel first.
|
||||
Future<void> updatePinRequired(bool value) =>
|
||||
|
||||
@@ -11,11 +11,26 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_item.dart';
|
||||
import '../utils/app_updater.dart';
|
||||
import '../viewmodels/bar_screen_view_model.dart';
|
||||
|
||||
class BarScreenView extends StatelessWidget {
|
||||
class BarScreenView extends StatefulWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<BarScreenViewModel>();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/settings.dart';
|
||||
import '../utils/app_update_util.dart';
|
||||
import '../viewmodels/pin_lock_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 {
|
||||
|
||||
@@ -632,6 +632,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
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
|
||||
# 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.1+1
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
@@ -49,6 +49,7 @@ dependencies:
|
||||
http: ^1.6.0
|
||||
package_info_plus: ^10.1.0
|
||||
open_filex: ^4.7.0
|
||||
ota_update: ^7.1.0
|
||||
# permission_handler: ^12.0.3
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user