Fix background service, add support for NBT instead of Json
This commit is contained in:
parent
a686412ec7
commit
4a8d515f4d
6 changed files with 289 additions and 24 deletions
|
@ -13,6 +13,7 @@
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class TTConsts {
|
||||||
static get SESSION_SERVER =>
|
static get SESSION_SERVER =>
|
||||||
"https://api.zontreck.com/timetrack/$UPDATE_CHANNEL/timetrack.php";
|
"https://api.zontreck.com/timetrack/$UPDATE_CHANNEL/timetrack.php";
|
||||||
|
|
||||||
static const VERSION = "1.0.0-beta.16";
|
static const VERSION = "1.0.0-beta.17";
|
||||||
|
|
||||||
static bool UPDATE_AVAILABLE = false;
|
static bool UPDATE_AVAILABLE = false;
|
||||||
static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta;
|
static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta;
|
||||||
|
|
256
lib/data.dart
256
lib/data.dart
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
@ -8,9 +9,19 @@ import 'package:floating_window_android/floating_window_android.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_background/flutter_background.dart';
|
import 'package:flutter_background/flutter_background.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:libac_dart/nbt/NbtIo.dart';
|
||||||
|
import 'package:libac_dart/nbt/NbtUtils.dart';
|
||||||
|
import 'package:libac_dart/nbt/SnbtIo.dart';
|
||||||
import 'package:libac_dart/nbt/Stream.dart';
|
import 'package:libac_dart/nbt/Stream.dart';
|
||||||
|
import 'package:libac_dart/nbt/impl/CompoundTag.dart';
|
||||||
|
import 'package:libac_dart/nbt/impl/DoubleTag.dart';
|
||||||
|
import 'package:libac_dart/nbt/impl/ListTag.dart';
|
||||||
|
import 'package:libac_dart/nbt/impl/StringTag.dart';
|
||||||
|
import 'package:libac_dart/utils/Converter.dart';
|
||||||
import 'package:libac_dart/utils/TimeUtils.dart';
|
import 'package:libac_dart/utils/TimeUtils.dart';
|
||||||
|
import 'package:libacflutter/nbt/nbtHelpers.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
class SessionData {
|
class SessionData {
|
||||||
static DateTime StartTime = DateTime(0);
|
static DateTime StartTime = DateTime(0);
|
||||||
|
@ -163,10 +174,14 @@ class SessionData {
|
||||||
"Background notification for keeping TimeTrack running in the background",
|
"Background notification for keeping TimeTrack running in the background",
|
||||||
notificationImportance: AndroidNotificationImportance.normal,
|
notificationImportance: AndroidNotificationImportance.normal,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool success = await FlutterBackground.initialize(
|
bool success = await FlutterBackground.initialize(
|
||||||
androidConfig: androidConfig,
|
androidConfig: androidConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
FlutterBackground.enableBackgroundExecution();
|
||||||
|
WakelockPlus.enable();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -214,6 +229,7 @@ class SessionData {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
positions.add(SmallPosition.fromPosition(pos));
|
positions.add(SmallPosition.fromPosition(pos));
|
||||||
|
SessionData.SaveCacheState();
|
||||||
|
|
||||||
SessionData.Calls.dispatch();
|
SessionData.Calls.dispatch();
|
||||||
});
|
});
|
||||||
|
@ -229,25 +245,165 @@ class SessionData {
|
||||||
|
|
||||||
EndTime = DateTime.now();
|
EndTime = DateTime.now();
|
||||||
|
|
||||||
var saveData = SaveData();
|
FlutterBackground.disableBackgroundExecution();
|
||||||
print(saveData);
|
WakelockPlus.disable();
|
||||||
|
|
||||||
|
var saveData = await _serializeToNBT();
|
||||||
|
ResetAppSession();
|
||||||
|
print(SnbtIo.writeToString(saveData));
|
||||||
|
Uint8List nbtData = await NbtIo.writeToStream(saveData);
|
||||||
|
|
||||||
Trips = [];
|
Trips = [];
|
||||||
positions = [];
|
positions = [];
|
||||||
|
|
||||||
Dio dio = Dio();
|
_upload(nbtData);
|
||||||
Map<String, dynamic> payload = {"cmd": "create", "data": saveData};
|
}
|
||||||
|
|
||||||
|
static Future<void> _upload(List<int> nbtData) async {
|
||||||
|
Dio dio = Dio();
|
||||||
|
|
||||||
|
Map<String, dynamic> payload = {
|
||||||
|
"cmd": "create",
|
||||||
|
"data": base64Encoder.encode(nbtData),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
var reply = await dio.post(
|
var reply = await dio.post(
|
||||||
TTConsts.SESSION_SERVER,
|
TTConsts.SESSION_SERVER,
|
||||||
data: json.encode(payload),
|
data: json.encode(payload),
|
||||||
);
|
);
|
||||||
|
if (reply.statusCode == null) {
|
||||||
|
throw Exception("Fatal error while uploading");
|
||||||
|
}
|
||||||
|
if (reply.statusCode! != 200) {
|
||||||
|
throw Exception("Fatal error while uploading");
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
||||||
if (replyJs["status"] == "ok") {
|
if (replyJs["status"] == "ok") {
|
||||||
print("Successful upload");
|
print("Successful upload");
|
||||||
LastSessionID = replyJs['session'] as String;
|
LastSessionID = replyJs['session'] as String;
|
||||||
Calls.dispatch();
|
Calls.dispatch();
|
||||||
}
|
}
|
||||||
|
} catch (E) {
|
||||||
|
// Retry in 2 seconds
|
||||||
|
DisplayError =
|
||||||
|
"Error: Something went wrong during upload. Retry in 5 seconds...";
|
||||||
|
Calls.dispatch();
|
||||||
|
|
||||||
|
Timer(Duration(seconds: 5), () {
|
||||||
|
_upload(nbtData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ResetAppSession() {
|
||||||
|
IsOnTheClock = false;
|
||||||
|
StartTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
Trips = [];
|
||||||
|
currentDelivery = null;
|
||||||
|
currentTrip = null;
|
||||||
|
positions = [];
|
||||||
|
EndTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
LastSessionID = "";
|
||||||
|
DisplayError = "";
|
||||||
|
IsReadOnly = false;
|
||||||
|
ContainsTripTimes = true;
|
||||||
|
|
||||||
|
Calls.dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function attempts to load the saved state from cache.
|
||||||
|
///
|
||||||
|
/// This will return true when a session exists; false when no session exists, or is empty.
|
||||||
|
static Future<bool> LoadSavedCacheState() async {
|
||||||
|
CompoundTag ct = await NBTHelper.GetNBT(name: "appstate");
|
||||||
|
// Restore various flags now.
|
||||||
|
if (ct.isEmpty) {
|
||||||
|
ResetAppSession();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deserialize(ct);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the current session based on various factors.
|
||||||
|
static Future<void> SaveCacheState() async {
|
||||||
|
CompoundTag ct = IsOnTheClock ? await _serializeToNBT() : CompoundTag();
|
||||||
|
await NBTHelper.CommitNBT(data: ct, name: "appstate");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _deserialize(CompoundTag ct) async {
|
||||||
|
IsOnTheClock = NbtUtils.readBoolean(ct, "inprog");
|
||||||
|
StartTime = DateTime.parse(ct.get("start")!.asString());
|
||||||
|
if (ct.containsKey("end")) {
|
||||||
|
EndTime = DateTime.parse(ct.get("end")!.asString());
|
||||||
|
} else {
|
||||||
|
EndTime = DateTime(0);
|
||||||
|
}
|
||||||
|
TotalPay = ct.get("totalPay")!.asDouble();
|
||||||
|
|
||||||
|
ListTag poses = ct.get("pos")! as ListTag;
|
||||||
|
for (var pos in poses.value) {
|
||||||
|
positions.add(await SmallPosition.fromNBT(pos.asCompoundTag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag trips = ct.get("trips") as ListTag;
|
||||||
|
for (var trip in trips.value) {
|
||||||
|
Trips.add(await Trip.fromNBT(trip.asCompoundTag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.containsKey("current_trip")) {
|
||||||
|
currentTrip = await Trip.fromNBT(ct.get("current_trip")!.asCompoundTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.containsKey("current_delivery")) {
|
||||||
|
currentDelivery = await Delivery.fromNBT(
|
||||||
|
ct.get("current_delivery")!.asCompoundTag(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This private function will turn all the data into NBT, for both the cache state, and newer usage, for storing it on the server in a more compact format.
|
||||||
|
static Future<CompoundTag> _serializeToNBT() async {
|
||||||
|
CompoundTag ct = CompoundTag();
|
||||||
|
|
||||||
|
NbtUtils.writeBoolean(ct, "inprog", IsOnTheClock);
|
||||||
|
// No need to write the contains trip times flag, it is set during deserialization. For inprog sessions, it will be set to true by the system.
|
||||||
|
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||||||
|
if (EndTime.year < 2000) {
|
||||||
|
// We have a end time
|
||||||
|
ct.put("end", StringTag.valueOf(EndTime.toIso8601String()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag posX = ListTag();
|
||||||
|
for (var pos in positions) {
|
||||||
|
posX.add(await pos.toNBT());
|
||||||
|
}
|
||||||
|
ct.put("pos", posX);
|
||||||
|
|
||||||
|
ListTag myTrips = ListTag();
|
||||||
|
for (var trip in Trips) {
|
||||||
|
myTrips.add(await trip.toNBT());
|
||||||
|
}
|
||||||
|
ct.put("trips", myTrips);
|
||||||
|
|
||||||
|
// This format supports saving current trip and current delivery.
|
||||||
|
if (currentDelivery != null) {
|
||||||
|
ct.put("current_delivery", await currentDelivery!.toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTrip != null) {
|
||||||
|
ct.put("current_trip", await currentTrip!.toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TotalPay != null) {
|
||||||
|
ct.put("totalPay", DoubleTag.valueOf(TotalPay!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, dynamic> SaveData() {
|
static Map<String, dynamic> SaveData() {
|
||||||
|
@ -283,7 +439,18 @@ class SessionData {
|
||||||
data: json.encode(payload),
|
data: json.encode(payload),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
String cType = reply.headers.value("Content-Type") ?? "application/json";
|
||||||
|
|
||||||
|
if (cType == "application/json") {
|
||||||
return LoadData(reply.data as Map<String, dynamic>);
|
return LoadData(reply.data as Map<String, dynamic>);
|
||||||
|
} else if (cType == "application/nbt") {
|
||||||
|
Uint8List lst = base64Encoder.decode(reply.data as String);
|
||||||
|
// Convert this to a CompoundTag
|
||||||
|
CompoundTag ct = await NbtIo.readFromStream(lst) as CompoundTag;
|
||||||
|
_deserialize(ct);
|
||||||
|
return true;
|
||||||
|
} else
|
||||||
|
return false;
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -389,7 +556,7 @@ class Delivery {
|
||||||
|
|
||||||
Map<String, dynamic> toJsonMap() {
|
Map<String, dynamic> toJsonMap() {
|
||||||
return {
|
return {
|
||||||
"start": StartTime.toString(),
|
"start": StartTime.toIso8601String(),
|
||||||
"endPos": endLocation?.toMap() ?? "incomplete",
|
"endPos": endLocation?.toMap() ?? "incomplete",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -407,6 +574,28 @@ class Delivery {
|
||||||
|
|
||||||
return delivery;
|
return delivery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CompoundTag> toNBT() async {
|
||||||
|
CompoundTag ct = CompoundTag();
|
||||||
|
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||||||
|
if (endLocation != null) {
|
||||||
|
ct.put("endPos", await endLocation!.toNBT());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Delivery> fromNBT(CompoundTag ct) async {
|
||||||
|
Delivery delivery = Delivery();
|
||||||
|
delivery.StartTime = DateTime.parse(ct.get("start")!.asString());
|
||||||
|
if (ct.containsKey("endPos")) {
|
||||||
|
delivery.endLocation = await SmallPosition.fromNBT(
|
||||||
|
ct.get("endPos") as CompoundTag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return delivery;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Trip {
|
class Trip {
|
||||||
|
@ -428,8 +617,8 @@ class Trip {
|
||||||
|
|
||||||
Map<String, dynamic> toJsonMap() {
|
Map<String, dynamic> toJsonMap() {
|
||||||
Map<String, Object> trip = {
|
Map<String, Object> trip = {
|
||||||
"start": StartTime.toString(),
|
"start": StartTime.toIso8601String(),
|
||||||
"end": EndTime.toString(),
|
"end": EndTime.toIso8601String(),
|
||||||
};
|
};
|
||||||
List<Map<String, dynamic>> dropOffs = [];
|
List<Map<String, dynamic>> dropOffs = [];
|
||||||
for (var delivery in deliveries) {
|
for (var delivery in deliveries) {
|
||||||
|
@ -446,8 +635,9 @@ class Trip {
|
||||||
trip.StartTime = DateTime.parse(jsx['start'] as String);
|
trip.StartTime = DateTime.parse(jsx['start'] as String);
|
||||||
if (jsx.containsKey("end")) {
|
if (jsx.containsKey("end")) {
|
||||||
trip.EndTime = DateTime.parse(jsx['end'] as String);
|
trip.EndTime = DateTime.parse(jsx['end'] as String);
|
||||||
} else
|
} else {
|
||||||
SessionData.ContainsTripTimes = false;
|
SessionData.ContainsTripTimes = false;
|
||||||
|
}
|
||||||
trip.deliveries = [];
|
trip.deliveries = [];
|
||||||
List<dynamic> dropOffs = jsx['deliveries'] as List<dynamic>;
|
List<dynamic> dropOffs = jsx['deliveries'] as List<dynamic>;
|
||||||
|
|
||||||
|
@ -457,6 +647,41 @@ class Trip {
|
||||||
|
|
||||||
return trip;
|
return trip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CompoundTag> toNBT() async {
|
||||||
|
CompoundTag ct = CompoundTag();
|
||||||
|
|
||||||
|
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||||||
|
if (EndTime.year < 2000) {
|
||||||
|
ct.put("end", StringTag.valueOf(EndTime.toIso8601String()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag drops = ListTag();
|
||||||
|
for (var drop in deliveries) {
|
||||||
|
drops.add(await drop.toNBT());
|
||||||
|
}
|
||||||
|
ct.put("deliveries", drops);
|
||||||
|
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Trip> fromNBT(CompoundTag tag) async {
|
||||||
|
Trip trip = Trip();
|
||||||
|
|
||||||
|
// Deserialize the Trip
|
||||||
|
trip.StartTime = DateTime.parse(tag.get("start")!.asString());
|
||||||
|
if (!tag.containsKey("end")) {
|
||||||
|
SessionData.ContainsTripTimes = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag drops = tag.get("deliveries")! as ListTag;
|
||||||
|
for (var drop in drops.value) {
|
||||||
|
Delivery del = await Delivery.fromNBT(drop.asCompoundTag());
|
||||||
|
trip.deliveries.add(del);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trip;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Callbacks {
|
class Callbacks {
|
||||||
|
@ -497,4 +722,19 @@ class SmallPosition {
|
||||||
longitude: map['longitude'] as double,
|
longitude: map['longitude'] as double,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CompoundTag> toNBT() async {
|
||||||
|
CompoundTag ct = CompoundTag();
|
||||||
|
ct.put("latitude", DoubleTag.valueOf(latitude));
|
||||||
|
ct.put("longitude", DoubleTag.valueOf(longitude));
|
||||||
|
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<SmallPosition> fromNBT(CompoundTag ct) async {
|
||||||
|
return SmallPosition(
|
||||||
|
latitude: ct.get("latitude")?.asDouble() ?? 0,
|
||||||
|
longitude: ct.get("longitude")?.asDouble() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,13 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
import 'package:timetrack/data.dart';
|
import 'package:timetrack/data.dart';
|
||||||
|
import 'package:timetrack/pages/HomePage.dart';
|
||||||
import 'package:timetrack/pages/MainApp.dart';
|
import 'package:timetrack/pages/MainApp.dart';
|
||||||
|
|
||||||
@pragma("vm:entry-point")
|
@pragma("vm:entry-point")
|
||||||
void serviceEntry() {
|
void serviceEntry() {
|
||||||
// This is where both the Background service and the Floating app window would enter the app.
|
|
||||||
Timer.periodic(Duration(seconds: 5), (timer) async {
|
|
||||||
// Run the location fetch, add to list, etc, etc.
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run the floater app here.
|
// Run the floater app here.
|
||||||
|
runApp(MaterialApp(debugShowCheckedModeBanner: false, home: OverlayWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
|
|
|
@ -30,6 +30,18 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
void call() {
|
void call() {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
|
if (SessionData.DisplayError.isNotEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(SessionData.DisplayError),
|
||||||
|
elevation: 8,
|
||||||
|
duration: Duration(seconds: 5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
SessionData.DisplayError = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -393,3 +405,17 @@ class _HomePageState extends State<HomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OverlayWidget extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _Overlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Overlay extends State<OverlayWidget> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.0-beta.16
|
version: 1.0.0-beta.17
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
@ -50,6 +50,7 @@ dependencies:
|
||||||
flutter_background: ^1.3.0+1
|
flutter_background: ^1.3.0+1
|
||||||
floating_window_android: ^1.0.0
|
floating_window_android: ^1.0.0
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
|
wakelock_plus: ^1.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue