feat: Implement background location tracking service
- Removed existing app icons from the asset catalog. - Added new `Contents.json` for asset catalog. - Updated `Info.plist` to include background modes and task scheduler identifiers. - Exposed BLE service in `ConnectionProvider` for background tracking. - Enhanced `MapTab` to support background location tracking with new service. - Added `BackgroundLocationService` to manage location updates in the background. - Updated `SettingsScreen` to allow users to configure GPS update distance and toggle background tracking. - Implemented functionality to send location updates via BLE in `MeshCoreBleService`. - Updated dependencies in `pubspec.yaml` for background service support. - Adjusted `TileCacheService` to ensure ObjectBox is initialized only once.
@@ -2,7 +2,18 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(flutter analyze:*)",
|
"Bash(flutter analyze:*)",
|
||||||
"Bash(flutter devices:*)"
|
"Bash(flutter devices:*)",
|
||||||
|
"Bash(flutter build:*)",
|
||||||
|
"Bash(flutter install:*)",
|
||||||
|
"Bash(open:*)",
|
||||||
|
"Bash(xcrun devicectl device uninstall app:*)",
|
||||||
|
"Bash(pod repo update:*)",
|
||||||
|
"Bash(pod install)",
|
||||||
|
"Bash(flutter clean:*)",
|
||||||
|
"Bash(flutter pub get:*)",
|
||||||
|
"Bash(flutter run:*)",
|
||||||
|
"Bash(nc:*)",
|
||||||
|
"Bash(pkill:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
<!-- Location permissions -->
|
<!-- Location permissions -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Foreground service for background location -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
<!-- Internet for map tiles -->
|
<!-- Internet for map tiles -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|||||||
4
devtools_options.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
|
- provider: true
|
||||||
@@ -34,6 +34,8 @@ PODS:
|
|||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_background_service_ios (0.0.3):
|
||||||
|
- Flutter
|
||||||
- flutter_blue_plus_darwin (0.0.2):
|
- flutter_blue_plus_darwin (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -66,6 +68,7 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
||||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||||
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
@@ -89,6 +92,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_background_service_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
||||||
flutter_blue_plus_darwin:
|
flutter_blue_plus_darwin:
|
||||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||||
flutter_compass:
|
flutter_compass:
|
||||||
@@ -113,6 +118,7 @@ SPEC CHECKSUMS:
|
|||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
|
||||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||||
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
|
|||||||
@@ -105,7 +105,6 @@
|
|||||||
2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */,
|
2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */,
|
11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -454,6 +453,7 @@
|
|||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
@@ -476,6 +476,7 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
@@ -489,7 +490,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = PM784W7B8X;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -578,6 +579,7 @@
|
|||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
@@ -607,6 +609,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -635,6 +638,7 @@
|
|||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
@@ -657,6 +661,7 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
@@ -672,7 +677,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = PM784W7B8X;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -695,7 +700,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = PM784W7B8X;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -1,122 +1,38 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"filename" : "AppIcon 11024x1024.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
"platform" : "ios",
|
||||||
"scale" : "2x"
|
"size" : "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"appearances" : [
|
||||||
"idiom" : "iphone",
|
{
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
"appearance" : "luminosity",
|
||||||
"scale" : "3x"
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "AppIcon 11024x1024 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"appearances" : [
|
||||||
"idiom" : "iphone",
|
{
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
"appearance" : "luminosity",
|
||||||
"scale" : "1x"
|
"value" : "tinted"
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
"size" : "29x29",
|
"filename" : "AppIcon 11024x1024 2.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "universal",
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
"platform" : "ios",
|
||||||
"scale" : "2x"
|
"size" : "1024x1024"
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
6
ios/Runner/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,5 +59,14 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>NSMotionUsageDescription</key>
|
<key>NSMotionUsageDescription</key>
|
||||||
<string>MeshCore SAR needs access to the compass to show your heading direction on the map</string>
|
<string>MeshCore SAR needs access to the compass to show your heading direction on the map</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>location</string>
|
||||||
|
<string>fetch</string>
|
||||||
|
</array>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>dev.flutter.background.refresh</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import '../utils/sar_message_parser.dart';
|
|||||||
class ConnectionProvider with ChangeNotifier {
|
class ConnectionProvider with ChangeNotifier {
|
||||||
final MeshCoreBleService _bleService = MeshCoreBleService();
|
final MeshCoreBleService _bleService = MeshCoreBleService();
|
||||||
|
|
||||||
|
/// Expose BLE service for background location tracking
|
||||||
|
MeshCoreBleService get bleService => _bleService;
|
||||||
|
|
||||||
DeviceInfo _deviceInfo = DeviceInfo();
|
DeviceInfo _deviceInfo = DeviceInfo();
|
||||||
DeviceInfo get deviceInfo => _deviceInfo;
|
DeviceInfo get deviceInfo => _deviceInfo;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../models/contact.dart';
|
|||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
import '../models/map_layer.dart';
|
import '../models/map_layer.dart';
|
||||||
import '../services/tile_cache_service.dart';
|
import '../services/tile_cache_service.dart';
|
||||||
|
import '../services/background_location_service.dart';
|
||||||
import '../widgets/map_markers.dart';
|
import '../widgets/map_markers.dart';
|
||||||
import '../widgets/map_debug_info.dart';
|
import '../widgets/map_debug_info.dart';
|
||||||
import 'map_management_screen.dart';
|
import 'map_management_screen.dart';
|
||||||
@@ -31,6 +32,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
final TileCacheService _tileCache = TileCacheService();
|
final TileCacheService _tileCache = TileCacheService();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
bool _isMapReady = false; // Track when map widget is actually rendered
|
||||||
MapLayer _currentLayer = MapLayer.openStreetMap;
|
MapLayer _currentLayer = MapLayer.openStreetMap;
|
||||||
Position? _currentPosition;
|
Position? _currentPosition;
|
||||||
double? _compassHeading; // Compass sensor heading
|
double? _compassHeading; // Compass sensor heading
|
||||||
@@ -38,8 +40,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
bool _showLegend = false;
|
bool _showLegend = false;
|
||||||
bool _showMapDebugInfo = false; // Toggle for debug info
|
bool _showMapDebugInfo = false; // Toggle for debug info
|
||||||
double _gpsUpdateDistance = 3.0; // meters
|
double _gpsUpdateDistance = 3.0; // meters
|
||||||
|
bool _backgroundTrackingEnabled = false; // Toggle for background tracking
|
||||||
StreamSubscription<Position>? _positionStreamSubscription;
|
StreamSubscription<Position>? _positionStreamSubscription;
|
||||||
StreamSubscription<CompassEvent>? _compassStreamSubscription;
|
StreamSubscription<CompassEvent>? _compassStreamSubscription;
|
||||||
|
final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
|
||||||
|
|
||||||
// Saved map position (loaded from SharedPreferences)
|
// Saved map position (loaded from SharedPreferences)
|
||||||
LatLng? _savedMapCenter;
|
LatLng? _savedMapCenter;
|
||||||
@@ -64,6 +68,13 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
mapProvider.addListener(_handleMapNavigation);
|
mapProvider.addListener(_handleMapNavigation);
|
||||||
|
|
||||||
|
// Initialize background location service with BLE service
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
_backgroundLocationService.initialize(appProvider.connectionProvider.bleService);
|
||||||
|
|
||||||
|
// Restore background tracking state
|
||||||
|
_restoreBackgroundTracking();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +93,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Rotate map if rotation mode is enabled and we have compass heading
|
// Rotate map if rotation mode is enabled and we have compass heading
|
||||||
// Only rotate if map is initialized
|
// Only rotate if map is ready
|
||||||
if (_rotateMarkerWithHeading && event.heading != null && _isInitialized) {
|
if (_rotateMarkerWithHeading && event.heading != null && _isMapReady) {
|
||||||
try {
|
try {
|
||||||
// Use moveAndRotate to set absolute rotation
|
// Use moveAndRotate to set absolute rotation
|
||||||
final camera = _mapController.camera;
|
final camera = _mapController.camera;
|
||||||
@@ -117,6 +128,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
|
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
|
||||||
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
|
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
|
||||||
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
|
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
|
||||||
|
_backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
|
||||||
|
|
||||||
// Store saved position for use in build
|
// Store saved position for use in build
|
||||||
if (lastLat != null && lastLon != null && lastZoom != null) {
|
if (lastLat != null && lastLon != null && lastZoom != null) {
|
||||||
@@ -138,15 +150,21 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
|
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
|
||||||
await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
|
await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
|
||||||
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
||||||
|
await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
|
||||||
await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer));
|
await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveMapPosition() async {
|
Future<void> _saveMapPosition() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
if (!_isMapReady) return;
|
||||||
final camera = _mapController.camera;
|
try {
|
||||||
await prefs.setDouble('map_last_latitude', camera.center.latitude);
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setDouble('map_last_longitude', camera.center.longitude);
|
final camera = _mapController.camera;
|
||||||
await prefs.setDouble('map_last_zoom', camera.zoom);
|
await prefs.setDouble('map_last_latitude', camera.center.latitude);
|
||||||
|
await prefs.setDouble('map_last_longitude', camera.center.longitude);
|
||||||
|
await prefs.setDouble('map_last_zoom', camera.zoom);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving map position: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestLocationPermission() async {
|
Future<void> _requestLocationPermission() async {
|
||||||
@@ -198,13 +216,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
// Rotate map if rotation mode is enabled and heading is available
|
// Rotate map if rotation mode is enabled and heading is available
|
||||||
// Heading of -1.0 means heading is unavailable
|
// Heading of -1.0 means heading is unavailable
|
||||||
if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
||||||
final camera = _mapController.camera;
|
try {
|
||||||
_mapController.moveAndRotate(
|
final camera = _mapController.camera;
|
||||||
camera.center,
|
_mapController.moveAndRotate(
|
||||||
camera.zoom,
|
camera.center,
|
||||||
-position.heading,
|
camera.zoom,
|
||||||
);
|
-position.heading,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Map not ready yet, ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -212,13 +234,18 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
void _handleMapNavigation() {
|
void _handleMapNavigation() {
|
||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
if (mapProvider.targetLocation != null && _isInitialized) {
|
if (mapProvider.targetLocation != null && _isMapReady) {
|
||||||
_mapController.move(
|
try {
|
||||||
mapProvider.targetLocation!,
|
_mapController.move(
|
||||||
mapProvider.targetZoom ?? _defaultZoom,
|
mapProvider.targetLocation!,
|
||||||
);
|
mapProvider.targetZoom ?? _defaultZoom,
|
||||||
// Clear the navigation request after handling
|
);
|
||||||
mapProvider.clearNavigation();
|
// Clear the navigation request after handling
|
||||||
|
mapProvider.clearNavigation();
|
||||||
|
} catch (e) {
|
||||||
|
// Map not ready yet, ignore
|
||||||
|
debugPrint('Map controller not ready for navigation: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +256,20 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
});
|
});
|
||||||
|
// Wait for the map to render, then mark it as ready
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
// Give the map widget one more frame to fully initialize
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isMapReady = true;
|
||||||
|
});
|
||||||
|
debugPrint('Map is now ready for controller operations');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error initializing tile cache: $e');
|
debugPrint('Error initializing tile cache: $e');
|
||||||
@@ -236,6 +277,19 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = true; // Continue without caching
|
_isInitialized = true; // Continue without caching
|
||||||
});
|
});
|
||||||
|
// Still mark map as ready after a delay
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isMapReady = true;
|
||||||
|
});
|
||||||
|
debugPrint('Map is now ready for controller operations');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +321,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safely get map rotation, returns 0.0 if map is not ready
|
||||||
|
double _getMapRotation() {
|
||||||
|
if (!_isMapReady) return 0.0;
|
||||||
|
try {
|
||||||
|
return _mapController.camera.rotation;
|
||||||
|
} catch (e) {
|
||||||
|
// Map controller not ready yet
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
|
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
|
||||||
final allPoints = <LatLng>[];
|
final allPoints = <LatLng>[];
|
||||||
|
|
||||||
@@ -346,23 +411,29 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToDownload(BuildContext context) {
|
void _navigateToDownload(BuildContext context) {
|
||||||
// Get current map bounds
|
if (!_isMapReady) return;
|
||||||
final bounds = _mapController.camera.visibleBounds;
|
|
||||||
final currentZoom = _mapController.camera.zoom.round();
|
|
||||||
|
|
||||||
// Navigate to Map Management screen with pre-populated data
|
try {
|
||||||
final appProvider = context.read<AppProvider>();
|
// Get current map bounds
|
||||||
Navigator.push(
|
final bounds = _mapController.camera.visibleBounds;
|
||||||
context,
|
final currentZoom = _mapController.camera.zoom.round();
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => MapManagementScreen(
|
// Navigate to Map Management screen with pre-populated data
|
||||||
tileCacheService: appProvider.tileCacheService,
|
final appProvider = context.read<AppProvider>();
|
||||||
initialLayer: _currentLayer,
|
Navigator.push(
|
||||||
initialBounds: bounds,
|
context,
|
||||||
initialZoom: currentZoom,
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MapManagementScreen(
|
||||||
|
tileCacheService: appProvider.tileCacheService,
|
||||||
|
initialLayer: _currentLayer,
|
||||||
|
initialBounds: bounds,
|
||||||
|
initialZoom: currentZoom,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} catch (e) {
|
||||||
|
debugPrint('Error accessing map camera: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showOptionsMenu(BuildContext context) {
|
void _showOptionsMenu(BuildContext context) {
|
||||||
@@ -416,17 +487,23 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_rotateMarkerWithHeading = value;
|
_rotateMarkerWithHeading = value;
|
||||||
// Reset map rotation when disabling
|
// Reset map rotation when disabling (only if map is ready)
|
||||||
final camera = _mapController.camera;
|
if (_isMapReady) {
|
||||||
if (!_rotateMarkerWithHeading) {
|
try {
|
||||||
_mapController.moveAndRotate(camera.center, camera.zoom, 0);
|
final camera = _mapController.camera;
|
||||||
} else if (_currentHeading != null) {
|
if (!_rotateMarkerWithHeading) {
|
||||||
// Apply current heading rotation when enabling (if heading is valid)
|
_mapController.moveAndRotate(camera.center, camera.zoom, 0);
|
||||||
_mapController.moveAndRotate(
|
} else if (_currentHeading != null) {
|
||||||
camera.center,
|
// Apply current heading rotation when enabling (if heading is valid)
|
||||||
camera.zoom,
|
_mapController.moveAndRotate(
|
||||||
-_currentHeading!,
|
camera.center,
|
||||||
);
|
camera.zoom,
|
||||||
|
-_currentHeading!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Map not ready yet, ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setModalState(() {});
|
setModalState(() {});
|
||||||
@@ -434,56 +511,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
// GPS Update Distance
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.gps_fixed),
|
|
||||||
title: const Text('GPS Update Distance'),
|
|
||||||
subtitle: Text('${_gpsUpdateDistance.toStringAsFixed(0)} meters'),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Slider(
|
|
||||||
value: _gpsUpdateDistance,
|
|
||||||
min: 1,
|
|
||||||
max: 20,
|
|
||||||
divisions: 19,
|
|
||||||
label: '${_gpsUpdateDistance.toStringAsFixed(0)}m',
|
|
||||||
onChanged: (value) {
|
|
||||||
setModalState(() {
|
|
||||||
_gpsUpdateDistance = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
setState(() {
|
|
||||||
_gpsUpdateDistance = value;
|
|
||||||
});
|
|
||||||
// Restart location stream with new distance
|
|
||||||
_restartLocationStream();
|
|
||||||
_saveSettings();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'1m',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'20m',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
// Map Debug Info toggle
|
// Map Debug Info toggle
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(Icons.developer_mode),
|
secondary: const Icon(Icons.developer_mode),
|
||||||
@@ -599,16 +626,60 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
// Rotate map if rotation mode is enabled and heading is available
|
// Rotate map if rotation mode is enabled and heading is available
|
||||||
// Heading of -1.0 means heading is unavailable
|
// Heading of -1.0 means heading is unavailable
|
||||||
if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
||||||
final camera = _mapController.camera;
|
try {
|
||||||
_mapController.moveAndRotate(
|
final camera = _mapController.camera;
|
||||||
camera.center,
|
_mapController.moveAndRotate(
|
||||||
camera.zoom,
|
camera.center,
|
||||||
-position.heading,
|
camera.zoom,
|
||||||
);
|
-position.heading,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Map not ready yet, ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update background tracking distance if active
|
||||||
|
if (_backgroundTrackingEnabled) {
|
||||||
|
_backgroundLocationService.updateDistanceThreshold(_gpsUpdateDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore background tracking state on app start
|
||||||
|
Future<void> _restoreBackgroundTracking() async {
|
||||||
|
if (_backgroundTrackingEnabled) {
|
||||||
|
await _startBackgroundTracking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start background location tracking
|
||||||
|
Future<void> _startBackgroundTracking() async {
|
||||||
|
final success = await _backgroundLocationService.startTracking(
|
||||||
|
distanceThreshold: _gpsUpdateDistance,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_backgroundTrackingEnabled = false;
|
||||||
|
});
|
||||||
|
_saveSettings();
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop background location tracking
|
||||||
|
Future<void> _stopBackgroundTracking() async {
|
||||||
|
await _backgroundLocationService.stopTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -654,7 +725,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
...MapMarkers.createTeamMemberMarkers(
|
...MapMarkers.createTeamMemberMarkers(
|
||||||
contactsWithLocation,
|
contactsWithLocation,
|
||||||
context,
|
context,
|
||||||
mapRotation: _mapController.camera.rotation,
|
mapRotation: _getMapRotation(),
|
||||||
onContactTap: (contact) {
|
onContactTap: (contact) {
|
||||||
_showDetailedCompassWithContact(
|
_showDetailedCompassWithContact(
|
||||||
context,
|
context,
|
||||||
@@ -667,7 +738,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
...MapMarkers.createSarMarkers(
|
...MapMarkers.createSarMarkers(
|
||||||
sarMarkers,
|
sarMarkers,
|
||||||
context,
|
context,
|
||||||
mapRotation: _mapController.camera.rotation,
|
mapRotation: _getMapRotation(),
|
||||||
onSarMarkerTap: (marker) {
|
onSarMarkerTap: (marker) {
|
||||||
_showDetailedCompassWithSarMarker(
|
_showDetailedCompassWithSarMarker(
|
||||||
context,
|
context,
|
||||||
@@ -766,7 +837,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
children: [
|
children: [
|
||||||
FloatingActionButton.small(
|
FloatingActionButton.small(
|
||||||
heroTag: 'center_map',
|
heroTag: 'center_map',
|
||||||
onPressed: () async {
|
onPressed: !_isMapReady ? null : () async {
|
||||||
// Force update GPS location and jump to it
|
// Force update GPS location and jump to it
|
||||||
try {
|
try {
|
||||||
final position = await Geolocator.getCurrentPosition(
|
final position = await Geolocator.getCurrentPosition(
|
||||||
@@ -818,7 +889,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Map debug info - bottom left
|
// Map debug info - bottom left
|
||||||
if (_showMapDebugInfo && _isInitialized)
|
if (_showMapDebugInfo && _isMapReady)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 16,
|
bottom: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/messages_provider.dart';
|
import '../providers/messages_provider.dart';
|
||||||
|
import '../providers/app_provider.dart';
|
||||||
|
import '../services/background_location_service.dart';
|
||||||
import '../utils/sample_data_generator.dart';
|
import '../utils/sample_data_generator.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
@@ -26,12 +28,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
late ThemeMode _selectedTheme;
|
late ThemeMode _selectedTheme;
|
||||||
PackageInfo? _packageInfo;
|
PackageInfo? _packageInfo;
|
||||||
bool _isLoadingSampleData = false;
|
bool _isLoadingSampleData = false;
|
||||||
|
double _gpsUpdateDistance = 10.0;
|
||||||
|
bool _backgroundTrackingEnabled = false;
|
||||||
|
final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedTheme = widget.currentTheme;
|
_selectedTheme = widget.currentTheme;
|
||||||
_loadPackageInfo();
|
_loadPackageInfo();
|
||||||
|
_loadLocationSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadPackageInfo() async {
|
Future<void> _loadPackageInfo() async {
|
||||||
@@ -43,6 +49,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLocationSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 10.0;
|
||||||
|
_backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize background location service
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
_backgroundLocationService.initialize(appProvider.connectionProvider.bleService);
|
||||||
|
|
||||||
|
// Restore background tracking state
|
||||||
|
if (_backgroundTrackingEnabled) {
|
||||||
|
_startBackgroundTracking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveLocationSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
||||||
|
await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveThemePreference(ThemeMode theme) async {
|
Future<void> _saveThemePreference(ThemeMode theme) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('theme_mode', theme.name);
|
await prefs.setString('theme_mode', theme.name);
|
||||||
@@ -136,6 +171,32 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _startBackgroundTracking() async {
|
||||||
|
final success = await _backgroundLocationService.startTracking(
|
||||||
|
distanceThreshold: _gpsUpdateDistance,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_backgroundTrackingEnabled = false;
|
||||||
|
});
|
||||||
|
_saveLocationSettings();
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopBackgroundTracking() async {
|
||||||
|
await _backgroundLocationService.stopTracking();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _clearSampleData() async {
|
Future<void> _clearSampleData() async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -195,6 +256,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
||||||
|
// Location Settings Section
|
||||||
|
_buildSectionHeader('Location'),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.gps_fixed),
|
||||||
|
title: const Text('GPS Update Distance'),
|
||||||
|
subtitle: Text('${_gpsUpdateDistance.toStringAsFixed(0)} meters'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showGpsDistanceDialog(),
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.location_on),
|
||||||
|
title: const Text('Background Location Tracking'),
|
||||||
|
subtitle: const Text('Send position updates to mesh network'),
|
||||||
|
value: _backgroundTrackingEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_backgroundTrackingEnabled = value;
|
||||||
|
if (value) {
|
||||||
|
_startBackgroundTracking();
|
||||||
|
} else {
|
||||||
|
_stopBackgroundTracking();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_saveLocationSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
// About Section
|
// About Section
|
||||||
_buildSectionHeader('About'),
|
_buildSectionHeader('About'),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -305,6 +394,78 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showGpsDistanceDialog() {
|
||||||
|
double tempDistance = _gpsUpdateDistance;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
|
title: const Text('GPS Update Distance'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Position updates sent every ${tempDistance.toStringAsFixed(0)} meters',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Slider(
|
||||||
|
value: tempDistance,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
divisions: 99,
|
||||||
|
label: '${tempDistance.toStringAsFixed(0)}m',
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() {
|
||||||
|
tempDistance = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'1m',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'100m',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_gpsUpdateDistance = tempDistance;
|
||||||
|
});
|
||||||
|
_saveLocationSettings();
|
||||||
|
|
||||||
|
// Update background tracking if active
|
||||||
|
if (_backgroundTrackingEnabled) {
|
||||||
|
_backgroundLocationService.updateDistanceThreshold(tempDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showThemeDialog() {
|
void _showThemeDialog() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
182
lib/services/background_location_service.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'meshcore_ble_service.dart';
|
||||||
|
|
||||||
|
/// Background location tracking service for SAR operations
|
||||||
|
/// Tracks user location and sends periodic updates via MeshCore BLE
|
||||||
|
class BackgroundLocationService {
|
||||||
|
static const String _prefKeyEnabled = 'background_tracking_enabled';
|
||||||
|
static const String _prefKeyDistance = 'background_tracking_distance';
|
||||||
|
|
||||||
|
MeshCoreBleService? _bleService;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
/// Initialize the service with BLE service reference
|
||||||
|
void initialize(MeshCoreBleService bleService) {
|
||||||
|
_bleService = bleService;
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start background location tracking
|
||||||
|
/// Returns true if successful, false otherwise
|
||||||
|
Future<bool> startTracking({double distanceThreshold = 10.0}) async {
|
||||||
|
if (!_isInitialized || _bleService == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check location permissions
|
||||||
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission == LocationPermission.deniedForever) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_prefKeyEnabled, true);
|
||||||
|
await prefs.setDouble(_prefKeyDistance, distanceThreshold);
|
||||||
|
|
||||||
|
// Initialize background service if not already running
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
final isRunning = await service.isRunning();
|
||||||
|
|
||||||
|
if (!isRunning) {
|
||||||
|
await _initializeBackgroundService();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the service
|
||||||
|
await service.startService();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop background location tracking
|
||||||
|
Future<void> stopTracking() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_prefKeyEnabled, false);
|
||||||
|
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
service.invoke('stop');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the distance threshold for location updates
|
||||||
|
void updateDistanceThreshold(double distance) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setDouble(_prefKeyDistance, distance);
|
||||||
|
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
service.invoke('updateDistance', {'distance': distance});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the background service
|
||||||
|
Future<void> _initializeBackgroundService() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
|
||||||
|
await service.configure(
|
||||||
|
iosConfiguration: IosConfiguration(
|
||||||
|
autoStart: false,
|
||||||
|
onForeground: _onStart,
|
||||||
|
onBackground: _onIosBackground,
|
||||||
|
),
|
||||||
|
androidConfiguration: AndroidConfiguration(
|
||||||
|
autoStart: false,
|
||||||
|
onStart: _onStart,
|
||||||
|
isForegroundMode: true,
|
||||||
|
autoStartOnBoot: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iOS background entry point
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static bool _onIosBackground(ServiceInstance service) {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Background service entry point
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
static void _onStart(ServiceInstance service) async {
|
||||||
|
// Ensure Flutter binding is initialized
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
|
Position? lastPosition;
|
||||||
|
StreamSubscription<Position>? positionSubscription;
|
||||||
|
double distanceThreshold = 10.0;
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final enabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
||||||
|
distanceThreshold = prefs.getDouble(_prefKeyDistance) ?? 10.0;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
service.stopSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start location tracking
|
||||||
|
try {
|
||||||
|
positionSubscription = Geolocator.getPositionStream(
|
||||||
|
locationSettings: LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: distanceThreshold.toInt(),
|
||||||
|
),
|
||||||
|
).listen((Position position) async {
|
||||||
|
// Calculate distance from last position
|
||||||
|
if (lastPosition != null) {
|
||||||
|
final distance = Geolocator.distanceBetween(
|
||||||
|
lastPosition!.latitude,
|
||||||
|
lastPosition!.longitude,
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only update if moved enough distance
|
||||||
|
if (distance < distanceThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store last position
|
||||||
|
lastPosition = position;
|
||||||
|
|
||||||
|
// Note: In a real implementation, we would need to communicate with
|
||||||
|
// the BLE service via isolate communication or shared storage.
|
||||||
|
// For now, this is a placeholder for the background tracking logic.
|
||||||
|
|
||||||
|
// Send location update via notification or data channel
|
||||||
|
service.invoke('location', {
|
||||||
|
'latitude': position.latitude,
|
||||||
|
'longitude': position.longitude,
|
||||||
|
'timestamp': position.timestamp.millisecondsSinceEpoch,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
service.stopSelf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for service commands
|
||||||
|
service.on('stop').listen((event) async {
|
||||||
|
await positionSubscription?.cancel();
|
||||||
|
service.stopSelf();
|
||||||
|
});
|
||||||
|
|
||||||
|
service.on('updateDistance').listen((event) {
|
||||||
|
if (event != null && event['distance'] != null) {
|
||||||
|
distanceThreshold = event['distance'] as double;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -386,6 +386,19 @@ class MeshCoreBleService {
|
|||||||
await _writeData(writer.toBytes());
|
await _writeData(writer.toBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send flood advertisement with current location
|
||||||
|
Future<void> sendFloodAdvertisement({
|
||||||
|
required double latitude,
|
||||||
|
required double longitude,
|
||||||
|
}) async {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert);
|
||||||
|
writer.writeByte(MeshCoreConstants.selfAdvertFlood);
|
||||||
|
writer.writeInt32LE((latitude * 10000).round());
|
||||||
|
writer.writeInt32LE((longitude * 10000).round());
|
||||||
|
await _writeData(writer.toBytes());
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispose resources
|
/// Dispose resources
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_txSubscription?.cancel();
|
_txSubscription?.cancel();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import '../models/map_layer.dart';
|
|||||||
class TileCacheService {
|
class TileCacheService {
|
||||||
static const String _storeName = 'meshcore_sar_tiles';
|
static const String _storeName = 'meshcore_sar_tiles';
|
||||||
|
|
||||||
|
// Global flag to ensure ObjectBox is only initialized once
|
||||||
|
static bool _objectBoxInitialized = false;
|
||||||
|
static final _initLock = <String, Future<void>>{};
|
||||||
|
|
||||||
late final FMTCStore _store;
|
late final FMTCStore _store;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _isDownloading = false;
|
bool _isDownloading = false;
|
||||||
@@ -15,8 +19,22 @@ class TileCacheService {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
// Ensure we only initialize ObjectBox once globally
|
||||||
|
if (!_objectBoxInitialized) {
|
||||||
|
// Use a lock to prevent concurrent initialization attempts
|
||||||
|
final initFuture = _initLock.putIfAbsent('objectbox', () async {
|
||||||
|
try {
|
||||||
|
await FMTCObjectBoxBackend().initialise();
|
||||||
|
_objectBoxInitialized = true;
|
||||||
|
} catch (e) {
|
||||||
|
// Already initialized or error - that's okay
|
||||||
|
_objectBoxInitialized = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await initFuture;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FMTCObjectBoxBackend().initialise();
|
|
||||||
_store = FMTCStore(_storeName);
|
_store = FMTCStore(_storeName);
|
||||||
await _store.manage.create();
|
await _store.manage.create();
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
|
|||||||
@@ -1,42 +1,74 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- file_picker (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- flutter_blue_plus_darwin (0.0.2):
|
- flutter_blue_plus_darwin (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- ObjectBox (1.9.2)
|
- geolocator_apple (1.2.0):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- ObjectBox (4.4.1)
|
||||||
- objectbox_flutter_libs (0.0.1):
|
- objectbox_flutter_libs (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- ObjectBox (= 1.9.2)
|
- ObjectBox (= 4.4.1)
|
||||||
|
- package_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`)
|
- objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`)
|
||||||
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- ObjectBox
|
- ObjectBox
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
file_picker:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
flutter_blue_plus_darwin:
|
flutter_blue_plus_darwin:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
|
geolocator_apple:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
|
||||||
objectbox_flutter_libs:
|
objectbox_flutter_libs:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos
|
||||||
|
package_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
|
share_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
ObjectBox: 46757a559717ab49b1cc0612eaff82b705acf394
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
objectbox_flutter_libs: ac606b51011c9ea99c0574bf2db0d27b053c750a
|
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
|
||||||
|
objectbox_flutter_libs: f51d18f6a4b5965c218843373b7dc5ed5e3a2008
|
||||||
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
|
|
||||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
|||||||
36
pubspec.lock
@@ -50,7 +50,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
@@ -158,6 +158,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.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.0"
|
||||||
|
flutter_background_service_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_background_service_android
|
||||||
|
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.1"
|
||||||
|
flutter_background_service_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_background_service_ios
|
||||||
|
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
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.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.2"
|
||||||
flutter_blue_plus:
|
flutter_blue_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -345,7 +377,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
|||||||
@@ -55,10 +55,6 @@ dependencies:
|
|||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
flutter_compass: ^0.8.0
|
flutter_compass: ^0.8.0
|
||||||
|
|
||||||
# Utilities
|
|
||||||
intl: ^0.20.2
|
|
||||||
collection: ^1.18.0
|
|
||||||
|
|
||||||
# File handling
|
# File handling
|
||||||
file_picker: ^8.1.4
|
file_picker: ^8.1.4
|
||||||
share_plus: ^10.1.3
|
share_plus: ^10.1.3
|
||||||
@@ -70,6 +66,9 @@ dependencies:
|
|||||||
# Package info
|
# Package info
|
||||||
package_info_plus: ^8.1.2
|
package_info_plus: ^8.1.2
|
||||||
|
|
||||||
|
# Background services
|
||||||
|
flutter_background_service: ^5.0.13
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||