feat: add background services and map status management
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<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_CONNECTED_DEVICE"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
<application
|
<application
|
||||||
@@ -47,6 +49,15 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
|
<!-- 前台服务配置 -->
|
||||||
|
<service
|
||||||
|
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||||
|
android:foregroundServiceType="connectedDevice|dataSync"
|
||||||
|
android:exported="false"
|
||||||
|
android:stopWithTask="false"
|
||||||
|
android:enabled="true"
|
||||||
|
tools:replace="android:exported"/>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:lbjconsole/screens/main_screen.dart';
|
import 'package:lbjconsole/screens/main_screen.dart';
|
||||||
import 'package:lbjconsole/util/train_type_util.dart';
|
import 'package:lbjconsole/util/train_type_util.dart';
|
||||||
import 'package:lbjconsole/util/loco_info_util.dart';
|
import 'package:lbjconsole/util/loco_info_util.dart';
|
||||||
import 'package:lbjconsole/util/loco_type_util.dart';
|
import 'package:lbjconsole/util/loco_type_util.dart';
|
||||||
import 'package:lbjconsole/services/loco_type_service.dart';
|
import 'package:lbjconsole/services/loco_type_service.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
import 'package:lbjconsole/services/database_service.dart';
|
||||||
|
import 'package:lbjconsole/services/background_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
await _initializeNotifications();
|
||||||
|
|
||||||
|
await BackgroundService.initialize();
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
TrainTypeUtil.initialize(),
|
TrainTypeUtil.initialize(),
|
||||||
LocoInfoUtil.initialize(),
|
LocoInfoUtil.initialize(),
|
||||||
@@ -18,6 +24,19 @@ void main() async {
|
|||||||
runApp(const LBJReceiverApp());
|
runApp(const LBJReceiverApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeNotifications() async {
|
||||||
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
|
const InitializationSettings initializationSettings = InitializationSettings(
|
||||||
|
android: initializationSettingsAndroid,
|
||||||
|
);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||||
|
}
|
||||||
|
|
||||||
class LBJReceiverApp extends StatelessWidget {
|
class LBJReceiverApp extends StatelessWidget {
|
||||||
const LBJReceiverApp({super.key});
|
const LBJReceiverApp({super.key});
|
||||||
|
|
||||||
|
|||||||
65
lib/models/map_state.dart
Normal file
65
lib/models/map_state.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
class MapState {
|
||||||
|
final double zoom;
|
||||||
|
final double centerLat;
|
||||||
|
final double centerLng;
|
||||||
|
final double bearing;
|
||||||
|
|
||||||
|
MapState({
|
||||||
|
required this.zoom,
|
||||||
|
required this.centerLat,
|
||||||
|
required this.centerLng,
|
||||||
|
required this.bearing,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'zoom': zoom,
|
||||||
|
'centerLat': centerLat,
|
||||||
|
'centerLng': centerLng,
|
||||||
|
'bearing': bearing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MapState.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MapState(
|
||||||
|
zoom: json['zoom']?.toDouble() ?? 10.0,
|
||||||
|
centerLat: json['centerLat']?.toDouble() ?? 39.9042,
|
||||||
|
centerLng: json['centerLng']?.toDouble() ?? 116.4074,
|
||||||
|
bearing: json['bearing']?.toDouble() ?? 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MapState copyWith({
|
||||||
|
double? zoom,
|
||||||
|
double? centerLat,
|
||||||
|
double? centerLng,
|
||||||
|
double? bearing,
|
||||||
|
}) {
|
||||||
|
return MapState(
|
||||||
|
zoom: zoom ?? this.zoom,
|
||||||
|
centerLat: centerLat ?? this.centerLat,
|
||||||
|
centerLng: centerLng ?? this.centerLng,
|
||||||
|
bearing: bearing ?? this.bearing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MapState(zoom: ' + zoom.toString() + ', centerLat: ' + centerLat.toString() + ', centerLng: ' + centerLng.toString() + ', bearing: ' + bearing.toString() + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is MapState &&
|
||||||
|
other.zoom == zoom &&
|
||||||
|
other.centerLat == centerLat &&
|
||||||
|
other.centerLng == centerLng &&
|
||||||
|
other.bearing == bearing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return zoom.hashCode ^ centerLat.hashCode ^ centerLng.hashCode ^ bearing.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import '../models/merged_record.dart';
|
|||||||
import '../services/database_service.dart';
|
import '../services/database_service.dart';
|
||||||
import '../models/train_record.dart';
|
import '../models/train_record.dart';
|
||||||
import '../services/merge_service.dart';
|
import '../services/merge_service.dart';
|
||||||
|
import '../models/map_state.dart';
|
||||||
|
import '../services/map_state_service.dart';
|
||||||
|
|
||||||
class HistoryScreen extends StatefulWidget {
|
class HistoryScreen extends StatefulWidget {
|
||||||
final Function(bool isEditing) onEditModeChanged;
|
final Function(bool isEditing) onEditModeChanged;
|
||||||
@@ -53,7 +55,6 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
loadRecords();
|
|
||||||
_scrollController.addListener(() {
|
_scrollController.addListener(() {
|
||||||
if (_scrollController.position.atEdge) {
|
if (_scrollController.position.atEdge) {
|
||||||
if (_scrollController.position.pixels == 0) {
|
if (_scrollController.position.pixels == 0) {
|
||||||
@@ -63,6 +64,9 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
if (_isAtTop) setState(() => _isAtTop = false);
|
if (_isAtTop) setState(() => _isAtTop = false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) loadRecords();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -72,7 +76,6 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadRecords({bool scrollToTop = true}) async {
|
Future<void> loadRecords({bool scrollToTop = true}) async {
|
||||||
if (mounted) setState(() => _isLoading = true);
|
|
||||||
try {
|
try {
|
||||||
final allRecords = await DatabaseService.instance.getAllRecords();
|
final allRecords = await DatabaseService.instance.getAllRecords();
|
||||||
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
@@ -80,15 +83,22 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
final hasDataChanged = _hasDataChanged(items);
|
||||||
_displayItems.clear();
|
|
||||||
_displayItems.addAll(items);
|
if (hasDataChanged) {
|
||||||
_isLoading = false;
|
setState(() {
|
||||||
});
|
_displayItems.clear();
|
||||||
if (scrollToTop && (_isAtTop) && _scrollController.hasClients) {
|
_displayItems.addAll(items);
|
||||||
_scrollController.animateTo(0.0,
|
_isLoading = false;
|
||||||
duration: const Duration(milliseconds: 300),
|
});
|
||||||
curve: Curves.easeOut);
|
|
||||||
|
if (scrollToTop && _isAtTop && _scrollController.hasClients) {
|
||||||
|
_scrollController.jumpTo(0.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_isLoading) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -96,9 +106,63 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> addNewRecord(TrainRecord newRecord) async {
|
||||||
|
try {
|
||||||
|
final settingsMap = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
|
_mergeSettings = MergeSettings.fromMap(settingsMap);
|
||||||
|
|
||||||
|
final isNewRecord = !_displayItems.any((item) {
|
||||||
|
if (item is TrainRecord) {
|
||||||
|
return item.uniqueId == newRecord.uniqueId;
|
||||||
|
} else if (item is MergedTrainRecord) {
|
||||||
|
return item.records.any((r) => r.uniqueId == newRecord.uniqueId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isNewRecord) return;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
final allRecords = await DatabaseService.instance.getAllRecords();
|
||||||
|
final items = MergeService.getMixedList(allRecords, _mergeSettings);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_displayItems.clear();
|
||||||
|
_displayItems.addAll(items);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_isAtTop && _scrollController.hasClients) {
|
||||||
|
_scrollController.jumpTo(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('添加新纪录失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasDataChanged(List<Object> newItems) {
|
||||||
|
if (_displayItems.length != newItems.length) return true;
|
||||||
|
|
||||||
|
for (int i = 0; i < _displayItems.length; i++) {
|
||||||
|
final oldItem = _displayItems[i];
|
||||||
|
final newItem = newItems[i];
|
||||||
|
|
||||||
|
if (oldItem.runtimeType != newItem.runtimeType) return true;
|
||||||
|
|
||||||
|
if (oldItem is TrainRecord && newItem is TrainRecord) {
|
||||||
|
if (oldItem.uniqueId != newItem.uniqueId) return true;
|
||||||
|
} else if (oldItem is MergedTrainRecord && newItem is MergedTrainRecord) {
|
||||||
|
if (oldItem.groupKey != newItem.groupKey) return true;
|
||||||
|
if (oldItem.records.length != newItem.records.length) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading && _displayItems.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (_displayItems.isEmpty) {
|
if (_displayItems.isEmpty) {
|
||||||
@@ -197,7 +261,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildExpandedMapForAll(mergedRecord.records),
|
_buildExpandedMapForAll(mergedRecord.records, mergedRecord.groupKey),
|
||||||
const Divider(color: Colors.white24, height: 24),
|
const Divider(color: Colors.white24, height: 24),
|
||||||
...mergedRecord.records.map((record) => _buildSubRecordItem(
|
...mergedRecord.records.map((record) => _buildSubRecordItem(
|
||||||
record, mergedRecord.latestRecord, _mergeSettings.groupBy)),
|
record, mergedRecord.latestRecord, _mergeSettings.groupBy)),
|
||||||
@@ -353,7 +417,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExpandedMapForAll(List<TrainRecord> records) {
|
Widget _buildExpandedMapForAll(List<TrainRecord> records, String groupKey) {
|
||||||
final positions = records
|
final positions = records
|
||||||
.map((record) => _parsePosition(record.positionInfo))
|
.map((record) => _parsePosition(record.positionInfo))
|
||||||
.whereType<LatLng>()
|
.whereType<LatLng>()
|
||||||
@@ -410,6 +474,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
positions: positions,
|
positions: positions,
|
||||||
center: bounds.center,
|
center: bounds.center,
|
||||||
zoom: zoomLevel,
|
zoom: zoomLevel,
|
||||||
|
groupKey: groupKey,
|
||||||
))
|
))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -664,6 +729,7 @@ class HistoryScreenState extends State<HistoryScreen> {
|
|||||||
key: ValueKey('map_${mapId}_$zoomLevel'),
|
key: ValueKey('map_${mapId}_$zoomLevel'),
|
||||||
position: position,
|
position: position,
|
||||||
zoom: zoomLevel,
|
zoom: zoomLevel,
|
||||||
|
recordId: record.uniqueId,
|
||||||
))
|
))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -803,11 +869,13 @@ _BoundaryBox _calculateBoundaryBoxIsolate(List<LatLng> positions) {
|
|||||||
class _DelayedMapWithMarker extends StatefulWidget {
|
class _DelayedMapWithMarker extends StatefulWidget {
|
||||||
final LatLng position;
|
final LatLng position;
|
||||||
final double zoom;
|
final double zoom;
|
||||||
|
final String recordId;
|
||||||
|
|
||||||
const _DelayedMapWithMarker({
|
const _DelayedMapWithMarker({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.zoom,
|
required this.zoom,
|
||||||
|
required this.recordId,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -815,11 +883,90 @@ class _DelayedMapWithMarker extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
|
class _DelayedMapWithMarkerState extends State<_DelayedMapWithMarker> {
|
||||||
|
late final MapController _mapController;
|
||||||
|
late final String _mapKey;
|
||||||
|
bool _isInitializing = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mapController = MapController();
|
||||||
|
_mapKey = MapStateService.instance.getSingleRecordMapKey(widget.recordId);
|
||||||
|
_initializeMapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeMapState() async {
|
||||||
|
final savedState = await MapStateService.instance.getMapState(_mapKey);
|
||||||
|
if (savedState != null && mounted) {
|
||||||
|
_mapController.move(
|
||||||
|
LatLng(savedState.centerLat, savedState.centerLng),
|
||||||
|
savedState.zoom,
|
||||||
|
);
|
||||||
|
if (savedState.bearing != 0.0) {
|
||||||
|
_mapController.rotate(savedState.bearing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCameraMove() {
|
||||||
|
if (_isInitializing) return;
|
||||||
|
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final state = MapState(
|
||||||
|
zoom: camera.zoom,
|
||||||
|
centerLat: camera.center.latitude,
|
||||||
|
centerLng: camera.center.longitude,
|
||||||
|
bearing: camera.rotation,
|
||||||
|
);
|
||||||
|
|
||||||
|
MapStateService.instance.saveMapState(_mapKey, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mapController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_isInitializing) {
|
||||||
|
return FlutterMap(
|
||||||
|
options: MapOptions(
|
||||||
|
initialCenter: widget.position,
|
||||||
|
initialZoom: widget.zoom,
|
||||||
|
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
||||||
|
),
|
||||||
|
mapController: _mapController,
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
userAgentPackageName: 'org.noxylva.lbjconsole'),
|
||||||
|
MarkerLayer(markers: [
|
||||||
|
Marker(
|
||||||
|
point: widget.position,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.white, width: 2)),
|
||||||
|
child:
|
||||||
|
const Icon(Icons.train, color: Colors.white, size: 20)))
|
||||||
|
])
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
options:
|
options: MapOptions(
|
||||||
MapOptions(initialCenter: widget.position, initialZoom: widget.zoom),
|
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
||||||
|
),
|
||||||
|
mapController: _mapController,
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
@@ -846,12 +993,14 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
|
|||||||
final List<LatLng> positions;
|
final List<LatLng> positions;
|
||||||
final LatLng center;
|
final LatLng center;
|
||||||
final double zoom;
|
final double zoom;
|
||||||
|
final String groupKey;
|
||||||
|
|
||||||
const _DelayedMultiMarkerMap({
|
const _DelayedMultiMarkerMap({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.positions,
|
required this.positions,
|
||||||
required this.center,
|
required this.center,
|
||||||
required this.zoom,
|
required this.zoom,
|
||||||
|
required this.groupKey,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -859,15 +1008,65 @@ class _DelayedMultiMarkerMap extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
|
class _DelayedMultiMarkerMapState extends State<_DelayedMultiMarkerMap> {
|
||||||
|
late final MapController _mapController;
|
||||||
|
late final String _mapKey;
|
||||||
|
bool _isInitializing = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mapController = MapController();
|
||||||
|
_mapKey = MapStateService.instance.getMergedRecordMapKey(widget.groupKey);
|
||||||
|
_initializeMapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeMapState() async {
|
||||||
|
final savedState = await MapStateService.instance.getMapState(_mapKey);
|
||||||
|
if (savedState != null && mounted) {
|
||||||
|
_mapController.move(
|
||||||
|
LatLng(savedState.centerLat, savedState.centerLng),
|
||||||
|
savedState.zoom,
|
||||||
|
);
|
||||||
|
if (savedState.bearing != 0.0) {
|
||||||
|
_mapController.rotate(savedState.bearing);
|
||||||
|
}
|
||||||
|
} else if (mounted) {
|
||||||
|
_mapController.move(widget.center, widget.zoom);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCameraMove() {
|
||||||
|
if (_isInitializing) return;
|
||||||
|
|
||||||
|
final camera = _mapController.camera;
|
||||||
|
final state = MapState(
|
||||||
|
zoom: camera.zoom,
|
||||||
|
centerLat: camera.center.latitude,
|
||||||
|
centerLng: camera.center.longitude,
|
||||||
|
bearing: camera.rotation,
|
||||||
|
);
|
||||||
|
|
||||||
|
MapStateService.instance.saveMapState(_mapKey, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mapController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: widget.center,
|
onPositionChanged: (position, hasGesture) => _onCameraMove(),
|
||||||
initialZoom: widget.zoom,
|
|
||||||
minZoom: 5,
|
minZoom: 5,
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
),
|
),
|
||||||
|
mapController: _mapController,
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:lbjconsole/screens/settings_screen.dart';
|
|||||||
import 'package:lbjconsole/services/ble_service.dart';
|
import 'package:lbjconsole/services/ble_service.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
import 'package:lbjconsole/services/database_service.dart';
|
||||||
import 'package:lbjconsole/services/notification_service.dart';
|
import 'package:lbjconsole/services/notification_service.dart';
|
||||||
|
import 'package:lbjconsole/services/background_service.dart';
|
||||||
import 'package:lbjconsole/themes/app_theme.dart';
|
import 'package:lbjconsole/themes/app_theme.dart';
|
||||||
|
|
||||||
class MainScreen extends StatefulWidget {
|
class MainScreen extends StatefulWidget {
|
||||||
@@ -39,6 +40,16 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
_bleService = BLEService();
|
_bleService = BLEService();
|
||||||
_bleService.initialize();
|
_bleService.initialize();
|
||||||
_initializeServices();
|
_initializeServices();
|
||||||
|
_checkAndStartBackgroundService();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkAndStartBackgroundService() async {
|
||||||
|
final settings = await DatabaseService.instance.getAllSettings() ?? {};
|
||||||
|
final backgroundServiceEnabled = (settings['backgroundServiceEnabled'] ?? 0) == 1;
|
||||||
|
|
||||||
|
if (backgroundServiceEnabled) {
|
||||||
|
await BackgroundService.startService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,7 +77,7 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
|
|||||||
_dataSubscription = _bleService.dataStream.listen((record) {
|
_dataSubscription = _bleService.dataStream.listen((record) {
|
||||||
_notificationService.showTrainNotification(record);
|
_notificationService.showTrainNotification(record);
|
||||||
if (_historyScreenKey.currentState != null) {
|
if (_historyScreenKey.currentState != null) {
|
||||||
_historyScreenKey.currentState!.loadRecords(scrollToTop: true);
|
_historyScreenKey.currentState!.addNewRecord(record);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:lbjconsole/models/merged_record.dart';
|
import 'package:lbjconsole/models/merged_record.dart';
|
||||||
import 'package:lbjconsole/services/database_service.dart';
|
import 'package:lbjconsole/services/database_service.dart';
|
||||||
import 'package:lbjconsole/services/ble_service.dart';
|
import 'package:lbjconsole/services/ble_service.dart';
|
||||||
|
import 'package:lbjconsole/services/background_service.dart';
|
||||||
import 'package:lbjconsole/themes/app_theme.dart';
|
import 'package:lbjconsole/themes/app_theme.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@@ -196,11 +197,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
value: _backgroundServiceEnabled,
|
value: _backgroundServiceEnabled,
|
||||||
onChanged: (value) {
|
onChanged: (value) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_backgroundServiceEnabled = value;
|
_backgroundServiceEnabled = value;
|
||||||
});
|
});
|
||||||
_saveSettings();
|
await _saveSettings();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
await BackgroundService.startService();
|
||||||
|
} else {
|
||||||
|
await BackgroundService.stopService();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
@@ -503,8 +510,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _shareData() async {
|
Future<void> _shareData() async {
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
@@ -530,7 +535,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (exportedPath != null) {
|
if (exportedPath != null) {
|
||||||
final file = File(exportedPath);
|
final file = File(exportedPath);
|
||||||
final fileName = file.path.split(Platform.pathSeparator).last;
|
final fileName = file.path.split(Platform.pathSeparator).last;
|
||||||
|
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
subject: 'LBJ Console Data',
|
subject: 'LBJ Console Data',
|
||||||
@@ -735,7 +740,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return Text(snapshot.data!, style: AppTheme.bodyMedium);
|
return Text(snapshot.data!, style: AppTheme.bodyMedium);
|
||||||
} else {
|
} else {
|
||||||
return const Text('v0.1.3-flutter', style: AppTheme.bodyMedium);
|
return const Text('v0.1.3-flutter',
|
||||||
|
style: AppTheme.bodyMedium);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
211
lib/services/background_service.dart
Normal file
211
lib/services/background_service.dart
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
|
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:lbjconsole/services/ble_service.dart';
|
||||||
|
|
||||||
|
const String _notificationChannelId = 'lbj_console_channel';
|
||||||
|
const String _notificationChannelName = 'LBJ Console 后台服务';
|
||||||
|
const String _notificationChannelDescription = '保持蓝牙连接稳定';
|
||||||
|
const int _notificationId = 114514;
|
||||||
|
|
||||||
|
class BackgroundService {
|
||||||
|
static final FlutterBackgroundService _service = FlutterBackgroundService();
|
||||||
|
static bool _isInitialized = false;
|
||||||
|
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
|
||||||
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||||
|
_notificationChannelId,
|
||||||
|
_notificationChannelName,
|
||||||
|
description: _notificationChannelDescription,
|
||||||
|
importance: Importance.low,
|
||||||
|
enableLights: false,
|
||||||
|
enableVibration: false,
|
||||||
|
playSound: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.configure(
|
||||||
|
androidConfiguration: AndroidConfiguration(
|
||||||
|
onStart: _onStart,
|
||||||
|
autoStart: true,
|
||||||
|
isForegroundMode: true,
|
||||||
|
notificationChannelId: _notificationChannelId,
|
||||||
|
initialNotificationTitle: 'LBJ Console',
|
||||||
|
initialNotificationContent: '蓝牙连接监控中',
|
||||||
|
foregroundServiceNotificationId: _notificationId,
|
||||||
|
),
|
||||||
|
iosConfiguration: IosConfiguration(
|
||||||
|
autoStart: true,
|
||||||
|
onForeground: _onStart,
|
||||||
|
onBackground: _onIosBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static void _onStart(ServiceInstance service) async {
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
|
if (service is AndroidServiceInstance) {
|
||||||
|
service.on('setAsForeground').listen((event) {
|
||||||
|
service.setAsForegroundService();
|
||||||
|
});
|
||||||
|
|
||||||
|
service.on('setAsBackground').listen((event) {
|
||||||
|
service.setAsBackgroundService();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
service.on('stopService').listen((event) {
|
||||||
|
service.stopSelf();
|
||||||
|
});
|
||||||
|
|
||||||
|
BLEService().initialize();
|
||||||
|
|
||||||
|
if (service is AndroidServiceInstance) {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
if (await service.isForegroundService()) {
|
||||||
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||||
|
_notificationChannelId,
|
||||||
|
_notificationChannelName,
|
||||||
|
description: _notificationChannelDescription,
|
||||||
|
importance: Importance.low,
|
||||||
|
enableLights: false,
|
||||||
|
enableVibration: false,
|
||||||
|
playSound: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
_notificationId,
|
||||||
|
'LBJ Console',
|
||||||
|
'蓝牙连接监控中',
|
||||||
|
NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
_notificationChannelId,
|
||||||
|
_notificationChannelName,
|
||||||
|
channelDescription: _notificationChannelDescription,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
ongoing: true,
|
||||||
|
autoCancel: false,
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
enableLights: false,
|
||||||
|
enableVibration: false,
|
||||||
|
playSound: false,
|
||||||
|
onlyAlertOnce: true,
|
||||||
|
setAsGroupSummary: false,
|
||||||
|
groupKey: 'lbj_console_group',
|
||||||
|
visibility: NotificationVisibility.public,
|
||||||
|
category: AndroidNotificationCategory.service,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('前台服务通知显示成功');
|
||||||
|
} catch (e) {
|
||||||
|
print('前台服务通知显示失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||||
|
if (service is AndroidServiceInstance) {
|
||||||
|
if (await service.isForegroundService()) {
|
||||||
|
try {
|
||||||
|
final bleService = BLEService();
|
||||||
|
final isConnected = bleService.isConnected;
|
||||||
|
final deviceStatus = bleService.deviceStatus;
|
||||||
|
|
||||||
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
_notificationId,
|
||||||
|
'LBJ Console',
|
||||||
|
isConnected ? '蓝牙已连接 - $deviceStatus' : '蓝牙未连接 - 自动重连中',
|
||||||
|
NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
_notificationChannelId,
|
||||||
|
_notificationChannelName,
|
||||||
|
channelDescription: _notificationChannelDescription,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
ongoing: true,
|
||||||
|
autoCancel: false,
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
enableLights: false,
|
||||||
|
enableVibration: false,
|
||||||
|
playSound: false,
|
||||||
|
onlyAlertOnce: true,
|
||||||
|
setAsGroupSummary: false,
|
||||||
|
groupKey: 'lbj_console_group',
|
||||||
|
visibility: NotificationVisibility.public,
|
||||||
|
category: AndroidNotificationCategory.service,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('前台服务通知更新失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static Future<bool> _onIosBackground(ServiceInstance service) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> startService() async {
|
||||||
|
await initialize();
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final isRunning = await service.isRunning();
|
||||||
|
if (!isRunning) {
|
||||||
|
service.startService();
|
||||||
|
}
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
service.startService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> stopService() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
service.invoke('stopService');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> isRunning() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
return await service.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setForegroundMode(bool isForeground) {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
if (isForeground) {
|
||||||
|
service.invoke('setAsForeground');
|
||||||
|
} else {
|
||||||
|
service.invoke('setAsBackground');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
lib/services/map_state_service.dart
Normal file
113
lib/services/map_state_service.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:lbjconsole/models/map_state.dart';
|
||||||
|
import 'database_service.dart';
|
||||||
|
|
||||||
|
class MapStateService {
|
||||||
|
static final MapStateService instance = MapStateService._internal();
|
||||||
|
factory MapStateService() => instance;
|
||||||
|
MapStateService._internal();
|
||||||
|
|
||||||
|
static const String _tableName = 'record_map_states';
|
||||||
|
|
||||||
|
final Map<String, MapState> _memoryCache = {};
|
||||||
|
|
||||||
|
Future<void> _ensureTableExists() async {
|
||||||
|
final db = await DatabaseService.instance.database;
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS $_tableName (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSingleRecordMapKey(String recordId) {
|
||||||
|
return "${recordId}_record_map";
|
||||||
|
}
|
||||||
|
|
||||||
|
String getMergedRecordMapKey(String groupKey) {
|
||||||
|
return "${groupKey}_group_map";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveMapState(String key, MapState state) async {
|
||||||
|
try {
|
||||||
|
_memoryCache[key] = state;
|
||||||
|
|
||||||
|
final db = await DatabaseService.instance.database;
|
||||||
|
await _ensureTableExists();
|
||||||
|
|
||||||
|
await db.insert(
|
||||||
|
_tableName,
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'state': jsonEncode(state.toJson()),
|
||||||
|
'updated_at': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('保存地图状态失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MapState?> getMapState(String key) async {
|
||||||
|
if (_memoryCache.containsKey(key)) {
|
||||||
|
return _memoryCache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final db = await DatabaseService.instance.database;
|
||||||
|
await _ensureTableExists();
|
||||||
|
|
||||||
|
final result = await db.query(
|
||||||
|
_tableName,
|
||||||
|
where: 'key = ?',
|
||||||
|
whereArgs: [key],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isNotEmpty) {
|
||||||
|
final stateJson = jsonDecode(result.first['state'] as String);
|
||||||
|
final state = MapState.fromJson(stateJson);
|
||||||
|
_memoryCache[key] = state;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('读取地图状态失败: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteMapState(String key) async {
|
||||||
|
_memoryCache.remove(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final db = await DatabaseService.instance.database;
|
||||||
|
await db.delete(
|
||||||
|
_tableName,
|
||||||
|
where: 'key = ?',
|
||||||
|
whereArgs: [key],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('删除地图状态失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllMapStates() async {
|
||||||
|
_memoryCache.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final db = await DatabaseService.instance.database;
|
||||||
|
await db.delete(_tableName);
|
||||||
|
} catch (e) {
|
||||||
|
print('清空地图状态失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearMemoryCache() {
|
||||||
|
_memoryCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
pubspec.lock
32
pubspec.lock
@@ -278,6 +278,38 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_background_service:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_background_service
|
||||||
|
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.0"
|
||||||
|
flutter_background_service_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_background_service_android
|
||||||
|
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.1"
|
||||||
|
flutter_background_service_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_background_service_ios
|
||||||
|
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.3"
|
||||||
|
flutter_background_service_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_background_service_platform_interface
|
||||||
|
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.2"
|
||||||
flutter_blue_plus:
|
flutter_blue_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ dependencies:
|
|||||||
file_picker: ^8.1.2
|
file_picker: ^8.1.2
|
||||||
package_info_plus: ^8.1.2
|
package_info_plus: ^8.1.2
|
||||||
msix: ^3.16.12
|
msix: ^3.16.12
|
||||||
|
flutter_background_service: ^5.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user