initial commit
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
45
.metadata
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: android
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: web
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
749
CLAUDE.md
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
# CLAUDE.md - MeshCore SAR App Technical Reference
|
||||||
|
|
||||||
|
This document provides technical details for AI assistants (like Claude) working with this codebase.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Type**: Flutter Mobile Application
|
||||||
|
**Purpose**: Search and Rescue (SAR) operations with MeshCore mesh network devices
|
||||||
|
**Architecture**: Provider-based state management with BLE communication
|
||||||
|
**Target Platforms**: iOS 13+, Android 5.0+ (API 21+)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── models/ # Data models
|
||||||
|
│ ├── contact.dart # Contact with telemetry
|
||||||
|
│ ├── contact_telemetry.dart # GPS, battery, temperature
|
||||||
|
│ ├── message.dart # Messages and SAR markers
|
||||||
|
│ ├── sar_marker.dart # SAR tactical markers
|
||||||
|
│ ├── device_info.dart # BLE device connection state
|
||||||
|
│ └── map_layer.dart # Map tile layer definitions
|
||||||
|
├── services/ # Business logic services
|
||||||
|
│ ├── meshcore_ble_service.dart # BLE communication
|
||||||
|
│ ├── meshcore_constants.dart # Protocol constants
|
||||||
|
│ ├── buffer_reader.dart # Binary protocol reader
|
||||||
|
│ ├── buffer_writer.dart # Binary protocol writer
|
||||||
|
│ ├── cayenne_lpp_parser.dart # Telemetry decoder
|
||||||
|
│ └── tile_cache_service.dart # Offline map tiles
|
||||||
|
├── providers/ # State management
|
||||||
|
│ ├── connection_provider.dart # BLE connection state
|
||||||
|
│ ├── contacts_provider.dart # Contact list management
|
||||||
|
│ ├── messages_provider.dart # Message history + SAR markers
|
||||||
|
│ ├── map_provider.dart # Map navigation state
|
||||||
|
│ └── app_provider.dart # Coordinator provider
|
||||||
|
├── screens/ # UI screens
|
||||||
|
│ ├── home_screen.dart # Main screen with tabs
|
||||||
|
│ ├── messages_tab.dart # Message list view
|
||||||
|
│ ├── contacts_tab.dart # Contact list view
|
||||||
|
│ └── map_tab.dart # Interactive map view
|
||||||
|
├── widgets/ # Reusable UI components
|
||||||
|
│ └── map_markers.dart # Custom map marker widgets
|
||||||
|
├── utils/ # Utilities
|
||||||
|
│ └── sar_message_parser.dart # Parse S:<emoji>:lat,lon format
|
||||||
|
└── main.dart # App entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- **flutter_blue_plus** (^2.0.0): BLE communication
|
||||||
|
- **flutter_map** (^8.2.2): Interactive mapping with OpenStreetMap
|
||||||
|
- **flutter_map_tile_caching** (^10.1.1): Offline map tile storage
|
||||||
|
- **provider** (^6.1.0): State management
|
||||||
|
- **latlong2** (^0.9.0): GPS coordinate handling
|
||||||
|
- **geolocator** (^14.0.2): Precise GPS location tracking
|
||||||
|
- **permission_handler** (^12.0.1): Runtime permissions
|
||||||
|
|
||||||
|
### MeshCore Protocol
|
||||||
|
|
||||||
|
The app implements the MeshCore BLE protocol based on https://github.com/meshcore-dev/meshcore.js
|
||||||
|
|
||||||
|
**BLE Service**: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||||
|
**RX Characteristic** (write): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||||
|
**TX Characteristic** (notify): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||||
|
|
||||||
|
#### Command Codes (Client → Device)
|
||||||
|
- `4`: Get contacts list
|
||||||
|
- `2`: Send text message
|
||||||
|
- `39`: Request telemetry
|
||||||
|
|
||||||
|
#### Response Codes (Device → Client)
|
||||||
|
- `3`: Contact information
|
||||||
|
- `7`: Message received
|
||||||
|
- `0x8B` (139): Telemetry response (Cayenne LPP format)
|
||||||
|
|
||||||
|
#### Binary Protocol Format
|
||||||
|
|
||||||
|
All protocol messages use little-endian byte order.
|
||||||
|
|
||||||
|
**Get Contacts Request**:
|
||||||
|
```
|
||||||
|
[0x04] - Command code
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contact Response**:
|
||||||
|
```
|
||||||
|
[0x03] - Response code
|
||||||
|
[32 bytes] - Public key
|
||||||
|
[1 byte] - Contact type (0=none, 1=chat, 2=repeater, 3=room)
|
||||||
|
[64 bytes] - Advertised name (null-terminated string)
|
||||||
|
[4 bytes] - Latitude (int32, divide by 10000 for degrees)
|
||||||
|
[4 bytes] - Longitude (int32, divide by 10000 for degrees)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Message Received**:
|
||||||
|
```
|
||||||
|
[0x07] - Response code
|
||||||
|
[1 byte] - Message type (0=contact, 1=channel)
|
||||||
|
[4 bytes] - Sender public key prefix
|
||||||
|
[4 bytes] - Recipient public key prefix
|
||||||
|
[2 bytes] - Text length (uint16)
|
||||||
|
[N bytes] - UTF-8 text
|
||||||
|
```
|
||||||
|
|
||||||
|
**Send Text Message**:
|
||||||
|
```
|
||||||
|
[0x02] - Command code
|
||||||
|
[32 bytes] - Recipient public key
|
||||||
|
[2 bytes] - Text length (uint16)
|
||||||
|
[N bytes] - UTF-8 text
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Telemetry**:
|
||||||
|
```
|
||||||
|
[0x27] (39) - Command code
|
||||||
|
[32 bytes] - Contact public key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Telemetry Response**:
|
||||||
|
```
|
||||||
|
[0x8B] (139) - Response code
|
||||||
|
[4 bytes] - Contact public key prefix
|
||||||
|
[N bytes] - Cayenne LPP payload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cayenne LPP Format
|
||||||
|
|
||||||
|
Telemetry data uses Cayenne Low Power Payload format:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Channel] [Type] [Data...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported Types**:
|
||||||
|
- `136` (0x88): GPS Location
|
||||||
|
- 4 bytes: Latitude (int32, divide by 10000)
|
||||||
|
- 4 bytes: Longitude (int32, divide by 10000)
|
||||||
|
- 4 bytes: Altitude (int32, divide by 100)
|
||||||
|
- `103` (0x67): Temperature Sensor
|
||||||
|
- 2 bytes: Temperature (int16, divide by 10 for °C)
|
||||||
|
- `2` (0x02): Analog Input (used for battery voltage)
|
||||||
|
- 2 bytes: Value (uint16, divide by 100 for volts)
|
||||||
|
|
||||||
|
### SAR Message Format
|
||||||
|
|
||||||
|
Special tactical markers embedded in messages:
|
||||||
|
|
||||||
|
```
|
||||||
|
S:<emoji>:<latitude>,<longitude>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recognized Emojis**:
|
||||||
|
- `🧑` or `👤`: Found Person
|
||||||
|
- `🔥`: Fire Location
|
||||||
|
- `🏕️` or `⛺`: Staging Area
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `S:🧑:46.0569,14.5058` - Person found at coordinates
|
||||||
|
- `S:🔥:46.0570,14.5060` - Fire detected
|
||||||
|
- `S:🏕️:46.0571,14.5062` - Base camp location
|
||||||
|
|
||||||
|
**Parsing Rules**:
|
||||||
|
- Must start with `S:`
|
||||||
|
- Single emoji character after first colon
|
||||||
|
- Comma-separated lat,lon after second colon
|
||||||
|
- Coordinates can be negative (e.g., `-12.3456`)
|
||||||
|
- No spaces allowed in format
|
||||||
|
|
||||||
|
## State Management Architecture
|
||||||
|
|
||||||
|
### Provider Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
MultiProvider
|
||||||
|
├── ConnectionProvider # BLE connection state
|
||||||
|
├── ContactsProvider # Contact list
|
||||||
|
├── MessagesProvider # Messages + SAR markers
|
||||||
|
├── MapProvider # Map navigation
|
||||||
|
└── AppProvider # Coordinator (uses all above)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
BLE Device → MeshCoreBleService → ConnectionProvider → AppProvider
|
||||||
|
↓
|
||||||
|
ContactsProvider
|
||||||
|
MessagesProvider
|
||||||
|
↓
|
||||||
|
UI
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Receiving a Message**
|
||||||
|
|
||||||
|
1. BLE device sends message via TX characteristic
|
||||||
|
2. `MeshCoreBleService._onDataReceived()` parses binary data
|
||||||
|
3. Calls `onMessageReceived` callback
|
||||||
|
4. `ConnectionProvider` receives message
|
||||||
|
5. `AppProvider` enhances message (check for SAR format)
|
||||||
|
6. `MessagesProvider.addMessage()` stores message
|
||||||
|
7. UI rebuilds via `Consumer<MessagesProvider>`
|
||||||
|
|
||||||
|
### Contact Types
|
||||||
|
|
||||||
|
```dart
|
||||||
|
enum ContactType {
|
||||||
|
none(0), // Unknown/invalid
|
||||||
|
chat(1), // Team member (shown on map)
|
||||||
|
repeater(2), // Network repeater node
|
||||||
|
room(3), // Communication channel/room
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Map Display Rules**:
|
||||||
|
- Only `ContactType.chat` contacts with valid GPS are shown on map
|
||||||
|
- Repeaters and rooms are listed in Contacts tab but not mapped
|
||||||
|
|
||||||
|
## Map Implementation
|
||||||
|
|
||||||
|
### Tile Layers
|
||||||
|
|
||||||
|
Three tile sources are supported via `MapLayer` enum:
|
||||||
|
|
||||||
|
1. **OpenStreetMap** (default)
|
||||||
|
- URL: `https://tile.openstreetmap.org/{z}/{x}/{y}.png`
|
||||||
|
- Max zoom: 19
|
||||||
|
- Best for street-level navigation
|
||||||
|
|
||||||
|
2. **OpenTopoMap**
|
||||||
|
- URL: `https://a.tile.opentopomap.org/{z}/{x}/{y}.png`
|
||||||
|
- Max zoom: 17
|
||||||
|
- Shows topographic features, elevation contours
|
||||||
|
|
||||||
|
3. **ESRI World Imagery**
|
||||||
|
- URL: `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}`
|
||||||
|
- Max zoom: 19
|
||||||
|
- Satellite imagery
|
||||||
|
|
||||||
|
### Offline Tile Caching
|
||||||
|
|
||||||
|
Uses `flutter_map_tile_caching` with ObjectBox backend:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Initialize cache
|
||||||
|
await FMTCObjectBoxBackend().initialise();
|
||||||
|
final store = FMTCStore('meshcore_sar_tiles');
|
||||||
|
await store.manage.create();
|
||||||
|
|
||||||
|
// Download region
|
||||||
|
final region = RectangleRegion(bounds);
|
||||||
|
await store.download.startForeground(region: region);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cache Behavior**:
|
||||||
|
- `CacheBehavior.cacheFirst`: Use cached tiles if available
|
||||||
|
- 30-day validity period
|
||||||
|
- Automatic background updates when online
|
||||||
|
|
||||||
|
### Map Markers
|
||||||
|
|
||||||
|
**Team Member Markers** (Blue):
|
||||||
|
- CircleAvatar with person icon
|
||||||
|
- Battery percentage badge at top
|
||||||
|
- Name label at bottom
|
||||||
|
- Tap to show details dialog
|
||||||
|
|
||||||
|
**SAR Event Markers** (Color-coded):
|
||||||
|
- Found Person: Green with 🧑
|
||||||
|
- Fire: Red with 🔥
|
||||||
|
- Staging Area: Orange with 🏕️
|
||||||
|
- Time ago label at top
|
||||||
|
- Type label at bottom
|
||||||
|
- Tap to show details dialog
|
||||||
|
|
||||||
|
### Map Navigation
|
||||||
|
|
||||||
|
**Navigation from Messages Tab**:
|
||||||
|
1. User taps SAR marker message
|
||||||
|
2. `MapProvider.navigateToLocation()` called
|
||||||
|
3. Target location and zoom stored in provider
|
||||||
|
4. Tab switches to Map
|
||||||
|
5. `MapTab._handleMapNavigation()` moves map
|
||||||
|
6. `MapProvider.clearNavigation()` resets state
|
||||||
|
|
||||||
|
**Zoom State Preservation**:
|
||||||
|
- Current zoom stored in `MapProvider`
|
||||||
|
- Maintained across tab switches
|
||||||
|
- Updated on user zoom gestures
|
||||||
|
|
||||||
|
### User Location Tracking
|
||||||
|
|
||||||
|
The app tracks the user's precise GPS location in real-time:
|
||||||
|
|
||||||
|
**Permission Setup** (iOS Info.plist):
|
||||||
|
```xml
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs location access to display team members and SAR markers on the map</string>
|
||||||
|
<key>NSLocationTemporaryPreciseUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs precise location for accurate positioning in SAR operations</string>
|
||||||
|
<key>NSLocationDefaultAccuracyReduced</key>
|
||||||
|
<false/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation** (lib/screens/map_tab.dart):
|
||||||
|
```dart
|
||||||
|
Position? _currentPosition;
|
||||||
|
bool _trackingLocation = false;
|
||||||
|
|
||||||
|
// Request permission and start tracking
|
||||||
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen to continuous position updates
|
||||||
|
Geolocator.getPositionStream(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: 10, // Update every 10 meters
|
||||||
|
),
|
||||||
|
).listen((Position position) {
|
||||||
|
setState(() => _currentPosition = position);
|
||||||
|
if (_trackingLocation) {
|
||||||
|
// Auto-center map on user location
|
||||||
|
_mapController.move(
|
||||||
|
LatLng(position.latitude, position.longitude),
|
||||||
|
_mapController.camera.zoom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**User Location Marker**:
|
||||||
|
- Blue pulsing circle showing current position
|
||||||
|
- Navigation icon indicating heading
|
||||||
|
- Tap location button to center map on user
|
||||||
|
- Tap again to enable tracking mode (map follows user movement)
|
||||||
|
|
||||||
|
### Map Legend
|
||||||
|
|
||||||
|
**Collapsible Legend** (lib/screens/map_tab.dart):
|
||||||
|
- Shows counts of team members and SAR markers
|
||||||
|
- Click to collapse to compact view
|
||||||
|
- Click again to expand
|
||||||
|
- Positioned in top-right corner
|
||||||
|
|
||||||
|
```dart
|
||||||
|
bool _showLegend = true;
|
||||||
|
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _showLegend = !_showLegend),
|
||||||
|
child: _showLegend
|
||||||
|
? _MapLegend(/* full legend with all counts */)
|
||||||
|
: Card(
|
||||||
|
child: Column([
|
||||||
|
Text('Legend'),
|
||||||
|
Icon(Icons.expand_more),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Development
|
||||||
|
|
||||||
|
### Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Run in debug mode
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Run with specific device
|
||||||
|
flutter run -d <device-id>
|
||||||
|
|
||||||
|
# Hot reload (during debug)
|
||||||
|
# Press 'r' in terminal
|
||||||
|
|
||||||
|
# Hot restart (during debug)
|
||||||
|
# Press 'R' in terminal
|
||||||
|
|
||||||
|
# Analyze code
|
||||||
|
flutter analyze
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
dart format lib/
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
flutter clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open Xcode workspace
|
||||||
|
open ios/Runner.xcworkspace
|
||||||
|
|
||||||
|
# Build from command line
|
||||||
|
flutter build ios --release
|
||||||
|
|
||||||
|
# Create IPA (requires signing)
|
||||||
|
flutter build ipa
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key iOS Files**:
|
||||||
|
- `ios/Runner/Info.plist`: Permissions and app configuration
|
||||||
|
- `ios/Podfile`: CocoaPods dependencies
|
||||||
|
- `ios/Runner.xcodeproj`: Xcode project
|
||||||
|
|
||||||
|
### Android Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug APK
|
||||||
|
flutter build apk --debug
|
||||||
|
|
||||||
|
# Release APK
|
||||||
|
flutter build apk --release
|
||||||
|
|
||||||
|
# App Bundle (for Play Store)
|
||||||
|
flutter build appbundle --release
|
||||||
|
|
||||||
|
# Split APKs by ABI
|
||||||
|
flutter build apk --split-per-abi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Android Files**:
|
||||||
|
- `android/app/src/main/AndroidManifest.xml`: Permissions and app configuration
|
||||||
|
- `android/app/build.gradle`: App-level build configuration
|
||||||
|
- `android/build.gradle`: Project-level build configuration
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding a New BLE Command
|
||||||
|
|
||||||
|
1. **Add command code** to `lib/services/meshcore_constants.dart`:
|
||||||
|
```dart
|
||||||
|
static const int cmdYourCommand = 42;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create command method** in `lib/services/meshcore_ble_service.dart`:
|
||||||
|
```dart
|
||||||
|
Future<void> yourCommand() async {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(MeshCoreConstants.cmdYourCommand);
|
||||||
|
await _sendCommand(writer.toBytes());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Handle response** in `_onDataReceived()`:
|
||||||
|
```dart
|
||||||
|
case MeshCoreConstants.respYourResponse:
|
||||||
|
// Parse response data
|
||||||
|
onYourCallback?.call(data);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New SAR Marker Type
|
||||||
|
|
||||||
|
1. **Update enum** in `lib/models/sar_marker.dart`:
|
||||||
|
```dart
|
||||||
|
enum SarMarkerType {
|
||||||
|
// existing types...
|
||||||
|
yourType('🆕', 'Your Type');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add to parser** in `lib/utils/sar_message_parser.dart`:
|
||||||
|
```dart
|
||||||
|
case '🆕':
|
||||||
|
return SarMarkerType.yourType;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add color** in `lib/widgets/map_markers.dart`:
|
||||||
|
```dart
|
||||||
|
case SarMarkerType.yourType:
|
||||||
|
return Colors.purple;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Update providers** in `lib/providers/messages_provider.dart`:
|
||||||
|
```dart
|
||||||
|
List<SarMarker> get yourTypeMarkers =>
|
||||||
|
sarMarkers.where((m) => m.type == SarMarkerType.yourType).toList();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New Map Layer
|
||||||
|
|
||||||
|
1. **Add to model** in `lib/models/map_layer.dart`:
|
||||||
|
```dart
|
||||||
|
static const yourLayer = MapLayer(
|
||||||
|
type: MapLayerType.yourLayer,
|
||||||
|
name: 'Your Layer',
|
||||||
|
urlTemplate: 'https://your-tile-server/{z}/{x}/{y}.png',
|
||||||
|
attribution: '© Your Attribution',
|
||||||
|
maxZoom: 19,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add to list**:
|
||||||
|
```dart
|
||||||
|
static const List<MapLayer> allLayers = [
|
||||||
|
openStreetMap,
|
||||||
|
openTopoMap,
|
||||||
|
esriWorldImagery,
|
||||||
|
yourLayer, // Add here
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Layer automatically appears in layer selector UI
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
flutter test test/widget_test.dart
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
flutter test --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run integration tests
|
||||||
|
flutter drive --target=test_driver/app.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Testing Checklist
|
||||||
|
|
||||||
|
**BLE Connection**:
|
||||||
|
- [ ] Scan discovers MeshCore devices
|
||||||
|
- [ ] Connection successful
|
||||||
|
- [ ] Device info displayed in status bar
|
||||||
|
- [ ] Disconnect works properly
|
||||||
|
|
||||||
|
**Contacts**:
|
||||||
|
- [ ] Contacts load after connection
|
||||||
|
- [ ] Contacts grouped by type
|
||||||
|
- [ ] Telemetry request works
|
||||||
|
- [ ] Battery/GPS displayed correctly
|
||||||
|
|
||||||
|
**Messages**:
|
||||||
|
- [ ] Messages received and displayed
|
||||||
|
- [ ] SAR markers highlighted
|
||||||
|
- [ ] Tap SAR marker navigates to map
|
||||||
|
- [ ] Message timestamps correct
|
||||||
|
|
||||||
|
**Map**:
|
||||||
|
- [ ] Map loads and displays tiles
|
||||||
|
- [ ] Team member markers appear
|
||||||
|
- [ ] SAR markers appear with correct colors
|
||||||
|
- [ ] Layer switching works
|
||||||
|
- [ ] Zoom/pan gestures work
|
||||||
|
- [ ] Marker tap shows details
|
||||||
|
- [ ] Offline tiles load
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### BLE Issues
|
||||||
|
|
||||||
|
**"Bluetooth adapter not available"**:
|
||||||
|
- Check device Bluetooth is on
|
||||||
|
- Verify permissions granted
|
||||||
|
- iOS: Check Info.plist has usage descriptions
|
||||||
|
- Android: Check AndroidManifest.xml has permissions
|
||||||
|
|
||||||
|
**"Connection failed"**:
|
||||||
|
- Device must support BLE
|
||||||
|
- Check service UUID matches
|
||||||
|
- Verify device is in range (<10m typically)
|
||||||
|
- Try scanning again
|
||||||
|
|
||||||
|
### Runtime Issues
|
||||||
|
|
||||||
|
**MissingPluginException for geolocator or other plugins**:
|
||||||
|
|
||||||
|
Example error:
|
||||||
|
```
|
||||||
|
MissingPluginException(No implementation found for method isLocationServiceEnabled
|
||||||
|
on channel flutter.baseflow.com/geolocator_apple)
|
||||||
|
```
|
||||||
|
|
||||||
|
This occurs when native plugin implementations aren't properly installed. Common after adding new dependencies.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# For iOS
|
||||||
|
cd ios
|
||||||
|
pod install
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Clean and rebuild
|
||||||
|
flutter clean
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
**If still failing on iOS**:
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
rm Podfile.lock
|
||||||
|
rm -rf Pods/
|
||||||
|
pod install
|
||||||
|
cd ..
|
||||||
|
flutter clean
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Issues
|
||||||
|
|
||||||
|
**iOS Pod Install Fails**:
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
rm Podfile.lock
|
||||||
|
rm -rf Pods/
|
||||||
|
pod install --repo-update
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
**CocoaPods ObjectBox Version Conflict**:
|
||||||
|
|
||||||
|
This error occurs when flutter_map_tile_caching updates its ObjectBox dependency but the cached Podfile.lock has an older version:
|
||||||
|
|
||||||
|
```
|
||||||
|
[!] CocoaPods could not find compatible versions for pod "ObjectBox":
|
||||||
|
In snapshot (Podfile.lock): ObjectBox (= 1.9.2)
|
||||||
|
In Podfile: objectbox_flutter_libs depends on ObjectBox (= 4.4.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Navigate to iOS directory
|
||||||
|
cd ios
|
||||||
|
|
||||||
|
# Remove cached dependency lock file
|
||||||
|
rm Podfile.lock
|
||||||
|
|
||||||
|
# Remove all installed pods
|
||||||
|
rm -rf Pods/
|
||||||
|
|
||||||
|
# Update CocoaPods repository (this may take a few minutes)
|
||||||
|
pod repo update
|
||||||
|
|
||||||
|
# Reinstall all pods with updated versions
|
||||||
|
pod install
|
||||||
|
|
||||||
|
# Return to project root
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Clean Flutter build cache
|
||||||
|
flutter clean
|
||||||
|
|
||||||
|
# Reinstall Flutter dependencies
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Run the app
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative solution** (if the above doesn't work):
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
rm Podfile.lock
|
||||||
|
rm -rf Pods/
|
||||||
|
pod deintegrate
|
||||||
|
pod cache clean --all
|
||||||
|
pod setup
|
||||||
|
pod install
|
||||||
|
cd ..
|
||||||
|
flutter clean
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: The `pod repo update` command can take 5-10 minutes as it downloads the entire CocoaPods specifications repository. This is normal.
|
||||||
|
|
||||||
|
**Android Gradle Timeout**:
|
||||||
|
```gradle
|
||||||
|
// android/gradle.properties
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.jvmargs=-Xmx4096m
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flutter Version Conflicts**:
|
||||||
|
```bash
|
||||||
|
flutter channel stable
|
||||||
|
flutter upgrade
|
||||||
|
flutter pub upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### BLE Communication
|
||||||
|
- Buffer incoming data to handle partial packets
|
||||||
|
- Throttle telemetry requests (max 1 per second per contact)
|
||||||
|
- Use `notifyListeners()` sparingly in providers
|
||||||
|
|
||||||
|
### Map Performance
|
||||||
|
- Limit visible markers (cluster if >100 markers)
|
||||||
|
- Use `repaint boundary` for marker widgets
|
||||||
|
- Implement marker virtualization for large datasets
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Dispose controllers in `dispose()` methods
|
||||||
|
- Clear message history after 1000 messages
|
||||||
|
- Implement tile cache size limits
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **BLE**: No authentication in current protocol - add encryption for production
|
||||||
|
- **Permissions**: Request minimum required permissions
|
||||||
|
- **Data**: No sensitive data should be logged
|
||||||
|
- **Network**: Use HTTPS for all tile sources
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential features to add:
|
||||||
|
|
||||||
|
1. **Message Sending**: UI to compose and send messages
|
||||||
|
2. **Route Recording**: Track team member paths over time
|
||||||
|
3. **Geofencing**: Alerts when team members enter/exit areas
|
||||||
|
4. **Voice Notes**: Attach audio to SAR markers
|
||||||
|
5. **Team Chat**: Real-time chat between team members
|
||||||
|
6. **Mission Plans**: Pre-loaded search patterns
|
||||||
|
7. **Statistics**: Coverage analysis, search time tracking
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Flutter Documentation](https://docs.flutter.dev/)
|
||||||
|
- [flutter_blue_plus API](https://pub.dev/documentation/flutter_blue_plus/)
|
||||||
|
- [flutter_map Documentation](https://docs.fleaflet.dev/)
|
||||||
|
- [MeshCore Protocol](https://github.com/meshcore-dev/meshcore.js)
|
||||||
|
- [Cayenne LPP Specification](https://developers.mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload)
|
||||||
|
- [Provider Package](https://pub.dev/packages/provider)
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For questions or contributions, please refer to the project repository or contact the development team.
|
||||||
343
README.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# MeshCore SAR App
|
||||||
|
|
||||||
|
A Flutter-based Search and Rescue (SAR) application that communicates with MeshCore mesh network devices via Bluetooth Low Energy (BLE).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time Messaging**: Receive and display messages from MeshCore mesh network
|
||||||
|
- **Contact Management**: Track team members, repeaters, and communication channels
|
||||||
|
- **SAR Markers**: Special location markers for found persons, fires, and staging areas
|
||||||
|
- **Interactive Map**: View team locations and SAR markers on an interactive map with multiple layer options:
|
||||||
|
- OpenStreetMap (default)
|
||||||
|
- OpenTopoMap (topographic)
|
||||||
|
- ESRI World Imagery (satellite)
|
||||||
|
- **Offline Support**: Map tiles cached for offline operation
|
||||||
|
- **Telemetry Tracking**: Monitor battery levels, GPS locations, and temperature for all contacts
|
||||||
|
- **BLE Communication**: Direct connection to MeshCore devices via Bluetooth
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before building the app, ensure you have:
|
||||||
|
|
||||||
|
- Flutter SDK 3.19.0 or higher
|
||||||
|
- Dart SDK 3.3.0 or higher
|
||||||
|
- Xcode 15+ (for iOS builds)
|
||||||
|
- Android Studio with Android SDK (for Android builds)
|
||||||
|
- CocoaPods (for iOS dependencies)
|
||||||
|
|
||||||
|
### Install Flutter
|
||||||
|
|
||||||
|
If you haven't installed Flutter yet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
git clone https://github.com/flutter/flutter.git -b stable
|
||||||
|
export PATH="$PATH:`pwd`/flutter/bin"
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
flutter doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
```bash
|
||||||
|
cd /path/to/meshcore-sar/meshcore_sar_app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify setup**:
|
||||||
|
```bash
|
||||||
|
flutter doctor
|
||||||
|
```
|
||||||
|
Fix any issues reported by Flutter Doctor before proceeding.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- macOS computer
|
||||||
|
- Xcode 15 or higher
|
||||||
|
- Apple Developer account (for physical device deployment)
|
||||||
|
- iOS device with iOS 13.0 or higher
|
||||||
|
|
||||||
|
#### Steps
|
||||||
|
|
||||||
|
1. **Open iOS folder in Xcode**:
|
||||||
|
```bash
|
||||||
|
open ios/Runner.xcworkspace
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure signing**:
|
||||||
|
- In Xcode, select the Runner project
|
||||||
|
- Go to "Signing & Capabilities"
|
||||||
|
- Select your development team
|
||||||
|
- Xcode will automatically handle provisioning
|
||||||
|
|
||||||
|
3. **Connect your iOS device** via USB
|
||||||
|
|
||||||
|
4. **Enable Developer Mode** on your iOS device:
|
||||||
|
- Settings → Privacy & Security → Developer Mode → Enable
|
||||||
|
|
||||||
|
5. **Trust your Mac** on the iOS device when prompted
|
||||||
|
|
||||||
|
6. **Run the app**:
|
||||||
|
```bash
|
||||||
|
# Run in debug mode
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Or build release
|
||||||
|
flutter build ios --release
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Install on device from Xcode**:
|
||||||
|
- Select your device in Xcode
|
||||||
|
- Click the "Run" button (▶️)
|
||||||
|
|
||||||
|
#### Common iOS Issues
|
||||||
|
|
||||||
|
**Problem**: "Runner has conflicting provisioning settings"
|
||||||
|
```bash
|
||||||
|
# Solution: Clean and rebuild
|
||||||
|
cd ios
|
||||||
|
pod deintegrate
|
||||||
|
pod install
|
||||||
|
cd ..
|
||||||
|
flutter clean
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Bluetooth permissions not working
|
||||||
|
- Ensure `Info.plist` contains all required permission keys
|
||||||
|
- Check that permissions are requested at runtime
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- Android Studio installed
|
||||||
|
- Android SDK 21 (Android 5.0) or higher
|
||||||
|
- Physical Android device or emulator
|
||||||
|
|
||||||
|
#### Steps
|
||||||
|
|
||||||
|
1. **Enable Developer Options** on your Android device:
|
||||||
|
- Settings → About Phone → Tap "Build Number" 7 times
|
||||||
|
- Go back → Developer Options → Enable "USB Debugging"
|
||||||
|
|
||||||
|
2. **Connect your Android device** via USB and authorize the computer
|
||||||
|
|
||||||
|
3. **Verify device connection**:
|
||||||
|
```bash
|
||||||
|
flutter devices
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the app**:
|
||||||
|
```bash
|
||||||
|
# Run in debug mode
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# Or specify device
|
||||||
|
flutter run -d <device-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Build APK**:
|
||||||
|
```bash
|
||||||
|
# Debug APK
|
||||||
|
flutter build apk --debug
|
||||||
|
|
||||||
|
# Release APK
|
||||||
|
flutter build apk --release
|
||||||
|
|
||||||
|
# App Bundle (for Play Store)
|
||||||
|
flutter build appbundle --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The APK will be located at:
|
||||||
|
- Debug: `build/app/outputs/flutter-apk/app-debug.apk`
|
||||||
|
- Release: `build/app/outputs/flutter-apk/app-release.apk`
|
||||||
|
|
||||||
|
6. **Install APK manually**:
|
||||||
|
```bash
|
||||||
|
# Install on connected device
|
||||||
|
flutter install
|
||||||
|
|
||||||
|
# Or use adb
|
||||||
|
adb install build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Common Android Issues
|
||||||
|
|
||||||
|
**Problem**: Gradle build fails
|
||||||
|
```bash
|
||||||
|
# Solution: Clean and rebuild
|
||||||
|
flutter clean
|
||||||
|
cd android
|
||||||
|
./gradlew clean
|
||||||
|
cd ..
|
||||||
|
flutter pub get
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Bluetooth permissions denied
|
||||||
|
- Ensure all Bluetooth permissions are in `AndroidManifest.xml`
|
||||||
|
- For Android 12+, request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` at runtime
|
||||||
|
|
||||||
|
**Problem**: "Execution failed for task ':app:minifyReleaseWithR8'"
|
||||||
|
```bash
|
||||||
|
# Add to android/app/build.gradle
|
||||||
|
android {
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running on Emulator/Simulator
|
||||||
|
|
||||||
|
### iOS Simulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available simulators
|
||||||
|
flutter emulators
|
||||||
|
|
||||||
|
# Launch a simulator
|
||||||
|
flutter emulators --launch <simulator-id>
|
||||||
|
|
||||||
|
# Run app
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: BLE functionality will not work on iOS Simulator. Use a physical device for testing.
|
||||||
|
|
||||||
|
### Android Emulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available emulators
|
||||||
|
flutter emulators
|
||||||
|
|
||||||
|
# Create new emulator in Android Studio:
|
||||||
|
# Tools → Device Manager → Create Virtual Device
|
||||||
|
|
||||||
|
# Launch emulator
|
||||||
|
flutter emulators --launch <emulator-id>
|
||||||
|
|
||||||
|
# Run app
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: BLE functionality requires specific emulator setup or physical device.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The app requires the following permissions:
|
||||||
|
|
||||||
|
### iOS (ios/Runner/Info.plist)
|
||||||
|
- `NSBluetoothAlwaysUsageDescription`: Bluetooth access for MeshCore devices
|
||||||
|
- `NSLocationWhenInUseUsageDescription`: Location access for map features
|
||||||
|
|
||||||
|
### Android (android/app/src/main/AndroidManifest.xml)
|
||||||
|
- `BLUETOOTH_SCAN`: Scan for BLE devices
|
||||||
|
- `BLUETOOTH_CONNECT`: Connect to BLE devices
|
||||||
|
- `ACCESS_FINE_LOCATION`: Required for BLE scanning
|
||||||
|
- `INTERNET`: Download map tiles
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Connect to MeshCore Device**:
|
||||||
|
- Tap "Connect" in the status bar
|
||||||
|
- Select your MeshCore device from the scan results
|
||||||
|
- Wait for connection confirmation
|
||||||
|
|
||||||
|
2. **View Messages**:
|
||||||
|
- Messages tab shows all received messages
|
||||||
|
- SAR marker messages are highlighted
|
||||||
|
- Tap SAR marker to view on map
|
||||||
|
|
||||||
|
3. **Manage Contacts**:
|
||||||
|
- Contacts tab shows team members, repeaters, and channels
|
||||||
|
- Tap contact to view details
|
||||||
|
- Use refresh button to request telemetry updates
|
||||||
|
|
||||||
|
4. **View Map**:
|
||||||
|
- Map tab displays team locations and SAR markers
|
||||||
|
- Tap layer selector to switch between map styles
|
||||||
|
- Use zoom controls or pinch gestures
|
||||||
|
- Tap markers for details
|
||||||
|
- Tap items in bottom list to navigate
|
||||||
|
|
||||||
|
## SAR Marker Format
|
||||||
|
|
||||||
|
Messages can contain SAR markers using this format:
|
||||||
|
```
|
||||||
|
S:<emoji>:<latitude>,<longitude>
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `S:🧑:46.0569,14.5058` - Found person
|
||||||
|
- `S:🔥:46.0570,14.5060` - Fire location
|
||||||
|
- `S:🏕️:46.0571,14.5062` - Staging area
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Models**: Data structures for contacts, messages, markers, telemetry
|
||||||
|
- **Services**: BLE communication, tile caching, protocol parsing
|
||||||
|
- **Providers**: State management using Provider pattern
|
||||||
|
- **Screens**: UI components for messages, contacts, map
|
||||||
|
- **Widgets**: Reusable UI elements like map markers
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Key packages used:
|
||||||
|
- `flutter_blue_plus`: BLE communication
|
||||||
|
- `flutter_map`: Interactive mapping
|
||||||
|
- `flutter_map_tile_caching`: Offline map tiles
|
||||||
|
- `provider`: State management
|
||||||
|
- `latlong2`: GPS coordinate handling
|
||||||
|
- `permission_handler`: Runtime permissions
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### App crashes on launch
|
||||||
|
```bash
|
||||||
|
flutter clean
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### BLE not working
|
||||||
|
- Ensure Bluetooth is enabled on device
|
||||||
|
- Check that all permissions are granted
|
||||||
|
- Verify MeshCore device is powered on and in range
|
||||||
|
|
||||||
|
### Map tiles not loading
|
||||||
|
- Check internet connection
|
||||||
|
- Verify tile URLs are accessible
|
||||||
|
- Clear tile cache and reload
|
||||||
|
|
||||||
|
### Build errors
|
||||||
|
```bash
|
||||||
|
# Complete clean rebuild
|
||||||
|
flutter clean
|
||||||
|
cd ios && pod deintegrate && pod install && cd ..
|
||||||
|
cd android && ./gradlew clean && cd ..
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is for Search and Rescue operations using MeshCore mesh network devices.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues related to:
|
||||||
|
- **Flutter**: https://flutter.dev/community
|
||||||
|
- **MeshCore Protocol**: https://github.com/meshcore-dev/meshcore.js
|
||||||
|
- **App Issues**: Open an issue in this repository
|
||||||
28
analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
44
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.meshcore.sar.meshcore_sar_app"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.meshcore.sar.meshcore_sar_app"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
59
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Bluetooth permissions -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<!-- Location permissions -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Internet for map tiles -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="meshcore_sar_app"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.meshcore.sar.meshcore_sar_app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
26
android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.9.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
34
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>13.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
43
ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '13.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
118
ios/Podfile.lock
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
PODS:
|
||||||
|
- DKImagePickerController/Core (4.3.9):
|
||||||
|
- DKImagePickerController/ImageDataManager
|
||||||
|
- DKImagePickerController/Resource
|
||||||
|
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||||
|
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||||
|
- DKImagePickerController/Core
|
||||||
|
- DKPhotoGallery
|
||||||
|
- DKImagePickerController/Resource (4.3.9)
|
||||||
|
- DKPhotoGallery (0.0.19):
|
||||||
|
- DKPhotoGallery/Core (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Model (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Preview (= 0.0.19)
|
||||||
|
- DKPhotoGallery/Resource (= 0.0.19)
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Core (0.0.19):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Preview
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Model (0.0.19):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Preview (0.0.19):
|
||||||
|
- DKPhotoGallery/Model
|
||||||
|
- DKPhotoGallery/Resource
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- DKPhotoGallery/Resource (0.0.19):
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- file_picker (0.0.1):
|
||||||
|
- DKImagePickerController/PhotoGallery
|
||||||
|
- Flutter
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- flutter_blue_plus_darwin (0.0.2):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- geolocator_apple (1.2.0):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- ObjectBox (4.4.1)
|
||||||
|
- objectbox_flutter_libs (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- ObjectBox (= 4.4.1)
|
||||||
|
- package_info_plus (0.4.5):
|
||||||
|
- Flutter
|
||||||
|
- path_provider_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- permission_handler_apple (9.3.0):
|
||||||
|
- Flutter
|
||||||
|
- SDWebImage (5.21.3):
|
||||||
|
- SDWebImage/Core (= 5.21.3)
|
||||||
|
- SDWebImage/Core (5.21.3)
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- SwiftyGif (5.4.5)
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||||
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
|
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
|
||||||
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- DKImagePickerController
|
||||||
|
- DKPhotoGallery
|
||||||
|
- ObjectBox
|
||||||
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
file_picker:
|
||||||
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
flutter_blue_plus_darwin:
|
||||||
|
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||||
|
geolocator_apple:
|
||||||
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
|
objectbox_flutter_libs:
|
||||||
|
:path: ".symlinks/plugins/objectbox_flutter_libs/ios"
|
||||||
|
package_info_plus:
|
||||||
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
|
path_provider_foundation:
|
||||||
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
permission_handler_apple:
|
||||||
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||||
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
|
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
|
||||||
|
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
|
||||||
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
749
ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
6E07C83B534F125C2CBE6788 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95A961E69DFB3804DF041D16 /* Pods_Runner.framework */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
B117D6FC7E214B5BDF078335 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
0A6847CB76DAC5A39547F092 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
3B8CF791ABE83A2374700CB3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
88D332C6C1988532993F282B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
8E8E561633FC42AD799C3EC6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
95A961E69DFB3804DF041D16 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
63128E4122D07543FBC5706D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
B117D6FC7E214B5BDF078335 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
6E07C83B534F125C2CBE6788 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3C28992F8CC08CA6C1F157CE /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0A6847CB76DAC5A39547F092 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
88D332C6C1988532993F282B /* Pods-Runner.release.xcconfig */,
|
||||||
|
3B8CF791ABE83A2374700CB3 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
8E8E561633FC42AD799C3EC6 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
3C28992F8CC08CA6C1F157CE /* Pods */,
|
||||||
|
9C44871016BF8D8E5F823EE9 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9C44871016BF8D8E5F823EE9 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
95A961E69DFB3804DF041D16 /* Pods_Runner.framework */,
|
||||||
|
5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
E57C8F1FC76A13F8392A3A72 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
63128E4122D07543FBC5706D /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
142118FEB8CC97859621AD22 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
0F5356670725D838348C6698 /* [CP] Embed Pods Frameworks */,
|
||||||
|
86AE9F22272E851DF6253AA1 /* [CP] Copy Pods Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
0F5356670725D838348C6698 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
142118FEB8CC97859621AD22 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
86AE9F22272E851DF6253AA1 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
E57C8F1FC76A13F8392A3A72 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = PM784W7B8X;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 8E8E561633FC42AD799C3EC6 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = PM784W7B8X;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = PM784W7B8X;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
10
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
13
ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
61
ios/Runner/Info.plist
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Meshcore Sar App</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>meshcore_sar_app</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs Bluetooth to communicate with MeshCore devices for Search & Rescue operations</string>
|
||||||
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs Bluetooth to communicate with MeshCore devices</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs location access to display team members and SAR markers on the map</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs location access for offline map functionality during field operations</string>
|
||||||
|
<key>NSLocationTemporaryPreciseUsageDescription</key>
|
||||||
|
<string>MeshCore SAR needs precise location for accurate positioning in SAR operations</string>
|
||||||
|
<key>NSLocationDefaultAccuracyReduced</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98db4168bc813778add322f796c1886948","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/geolocator_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"geolocator_apple","INFOPLIST_FILE":"Target Support Files/geolocator_apple/ResourceBundle-geolocator_apple_privacy-geolocator_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"11.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"geolocator_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e984f90d418214f372cb5652188b6c03ab7","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98eaf9cc3cd0e4bd5b5081a977e6dd395a","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/geolocator_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"geolocator_apple","INFOPLIST_FILE":"Target Support Files/geolocator_apple/ResourceBundle-geolocator_apple_privacy-geolocator_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"11.0","PRODUCT_NAME":"geolocator_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98ef1f6c2c0471787bb8c754d246f4e6f8","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98eaf9cc3cd0e4bd5b5081a977e6dd395a","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/geolocator_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"geolocator_apple","INFOPLIST_FILE":"Target Support Files/geolocator_apple/ResourceBundle-geolocator_apple_privacy-geolocator_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"11.0","PRODUCT_NAME":"geolocator_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98e7e407f8bb5172b2db2019bb528593a9","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9884f731f5ac95fb5b060ee50b794122f2","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e982ce1691e61b20960a2ea685395560596","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e982d53e45b4eb2667e94d388d54e1ed196","guid":"bfdfe7dc352907fc980b868725387e98276a88038657a871d360f4cbfebc876d"}],"guid":"bfdfe7dc352907fc980b868725387e98cd4b69f26505cfa442ef066f8d043cc4","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98e1aba8ff8dc833f2269ce0a7182533b3","name":"geolocator_apple-geolocator_apple_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e980ae07e1806c3af2f5550d2e89780c766","name":"geolocator_apple_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e984da6ed4b7d87dcd4c1eb08c9fc111bbf","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e980bc977b873df9b0e01b3c822e5c77429","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9831c1531f7c6e1b7490400c5fb70a79a5","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98b75274b69084014a6a5ac37ea7a9d4bc","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9831c1531f7c6e1b7490400c5fb70a79a5","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e988b8e6347e534cb57e9bb1b22dc47b716","name":"Release"}],"buildPhases":[],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e988447dbc9b1396f2f8b874665d7663344","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/package_info_plus","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"package_info_plus","INFOPLIST_FILE":"Target Support Files/package_info_plus/ResourceBundle-package_info_plus_privacy-package_info_plus-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"package_info_plus_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98ea615c640c384edc0e21ed1692291823","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982618b5a51037a32a885f4497ede4dcf3","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/package_info_plus","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"package_info_plus","INFOPLIST_FILE":"Target Support Files/package_info_plus/ResourceBundle-package_info_plus_privacy-package_info_plus-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"package_info_plus_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e984e23d167444e6f055f38dd7a69a18dd7","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982618b5a51037a32a885f4497ede4dcf3","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/package_info_plus","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"package_info_plus","INFOPLIST_FILE":"Target Support Files/package_info_plus/ResourceBundle-package_info_plus_privacy-package_info_plus-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"package_info_plus_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9887dda0bca521f625d58f0030dc1ef821","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98bd08ace1fe49221266781def1534d061","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e987a1a049f7fcc890c93b2d478f2ebcdb3","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e987da0dca8e6b6cfc96465df580c4b6738","guid":"bfdfe7dc352907fc980b868725387e9840ce923bbef0d975d1f1dd0030483db8"}],"guid":"bfdfe7dc352907fc980b868725387e98d2fed6345309fbc2a5f123796da16852","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987b6c2f882d164ef4f3c76673562685a1","name":"package_info_plus-package_info_plus_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e982a9852aa81a16cf5578d0e8c78b5679a","name":"package_info_plus_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c5ae77c3ed9c6121cedd0cc76f43439d","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e9805a7aa5f1f9ee500ad450f318d7c6bb9","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98bc34136e26d305900a48578fde6599dd","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e982ce0cb00cdbfda3dc319c833ad6ae38c","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98bc34136e26d305900a48578fde6599dd","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","IPHONEOS_DEPLOYMENT_TARGET":"12.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e982a78a0117e7c8676789927b020dc3cdc","name":"Release"}],"buildPhases":[{"alwaysOutOfDate":"false","alwaysRunForInstallHdrs":"false","buildFiles":[],"emitEnvironment":"false","guid":"bfdfe7dc352907fc980b868725387e9833b1d8959324e2199aa08cec6b52aa6c","inputFileListPaths":["${PODS_ROOT}/Target Support Files/ObjectBox/ObjectBox-xcframeworks-input-files.xcfilelist"],"inputFilePaths":[],"name":"[CP] Copy XCFrameworks","originalObjectID":"CF5046B1965C17C425C68434C5F4930A","outputFileListPaths":["${PODS_ROOT}/Target Support Files/ObjectBox/ObjectBox-xcframeworks-output-files.xcfilelist"],"outputFilePaths":[],"sandboxingOverride":"basedOnBuildSetting","scriptContents":"\"${PODS_ROOT}/Target Support Files/ObjectBox/ObjectBox-xcframeworks.sh\"\n","shellPath":"/bin/sh","type":"com.apple.buildphase.shell-script"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9899d473c1bf2fb661137a0585385808b4","name":"ObjectBox","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e985d6c98d572eacea560ef85525944bc53","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9880add09333c603618ba57301ef150366","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e983214452bd1fb01e59882991c3a0c6270","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9847c9860dbbe72e455fd62321ab015d07","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e983214452bd1fb01e59882991c3a0c6270","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"12.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98015187516b84ba9413737c5495e2630f","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98e17b6ebc5129f9508fe634174cc60540","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98b11ffdc4603d340d255431d37eda7cff","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98f875f9d760fe29ff006006bf8a5e29db","guid":"bfdfe7dc352907fc980b868725387e98d6567728f9dc045f8d11b87239454a17"}],"guid":"bfdfe7dc352907fc980b868725387e983844082810a6bc94d89439820fcf9464","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986e649604f74c414a7c2dbe5ef4cc4e75","name":"path_provider_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98b2d96934906ee3fbaef29d6d94829467","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98f766aa246d1b90c876b6e3208c1336fe","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987f457f86a057c20b67c9b014731ae27e","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98e895a43ffa62f210c41ab4ed9f6b378d","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987f457f86a057c20b67c9b014731ae27e","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","EXPANDED_CODE_SIGN_IDENTITY":"-","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"9.0","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9859313236931631f81cee652c06e89e82","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e987f3961a0227401794e66b2ad691417b2","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98fd3f2136c497a33f11909a4555f0b9de","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e982eac7f03e53587ac5e1b2814ff7688b5","guid":"bfdfe7dc352907fc980b868725387e98692c4078ff878e239810d50f748be088"}],"guid":"bfdfe7dc352907fc980b868725387e98d8dfe8c885fcb7f295edd748d6ae4f67","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9802f35ab680609a626ebd2ddd692a3822","name":"permission_handler_apple-permission_handler_apple_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e983e9a904e8a35cb34b69458780be142b3","name":"permission_handler_apple_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/dz0ny/meshcore-sar/meshcore_sar_app/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=8087a2fc1456ece247eee30e0163f06f_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
|
||||||
104
lib/main.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'providers/connection_provider.dart';
|
||||||
|
import 'providers/contacts_provider.dart';
|
||||||
|
import 'providers/messages_provider.dart';
|
||||||
|
import 'providers/map_provider.dart';
|
||||||
|
import 'providers/app_provider.dart';
|
||||||
|
import 'services/tile_cache_service.dart';
|
||||||
|
import 'screens/home_screen.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MeshCoreSarApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeshCoreSarApp extends StatelessWidget {
|
||||||
|
const MeshCoreSarApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
// Core providers
|
||||||
|
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ContactsProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => MessagesProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => MapProvider()),
|
||||||
|
|
||||||
|
// Tile cache service
|
||||||
|
Provider(create: (_) => TileCacheService()),
|
||||||
|
|
||||||
|
// App provider that coordinates everything
|
||||||
|
ChangeNotifierProxyProvider4<ConnectionProvider, ContactsProvider,
|
||||||
|
MessagesProvider, TileCacheService, AppProvider>(
|
||||||
|
create: (context) => AppProvider(
|
||||||
|
connectionProvider: context.read<ConnectionProvider>(),
|
||||||
|
contactsProvider: context.read<ContactsProvider>(),
|
||||||
|
messagesProvider: context.read<MessagesProvider>(),
|
||||||
|
tileCacheService: context.read<TileCacheService>(),
|
||||||
|
),
|
||||||
|
update: (context, conn, contacts, messages, tileCache, previous) =>
|
||||||
|
previous ??
|
||||||
|
AppProvider(
|
||||||
|
connectionProvider: conn,
|
||||||
|
contactsProvider: contacts,
|
||||||
|
messagesProvider: messages,
|
||||||
|
tileCacheService: tileCache,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
title: 'MeshCore SAR',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.orange,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
darkTheme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.orange,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
|
home: const HomeScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/models/contact.dart
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'contact_telemetry.dart';
|
||||||
|
|
||||||
|
/// MeshCore contact types
|
||||||
|
enum ContactType {
|
||||||
|
none(0),
|
||||||
|
chat(1),
|
||||||
|
repeater(2),
|
||||||
|
room(3);
|
||||||
|
|
||||||
|
const ContactType(this.value);
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
static ContactType fromValue(int value) {
|
||||||
|
return ContactType.values.firstWhere(
|
||||||
|
(e) => e.value == value,
|
||||||
|
orElse: () => ContactType.none,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case ContactType.chat:
|
||||||
|
return 'Chat';
|
||||||
|
case ContactType.repeater:
|
||||||
|
return 'Repeater';
|
||||||
|
case ContactType.room:
|
||||||
|
return 'Room';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MeshCore contact model
|
||||||
|
class Contact {
|
||||||
|
final Uint8List publicKey;
|
||||||
|
final ContactType type;
|
||||||
|
final int flags;
|
||||||
|
final int outPathLen;
|
||||||
|
final Uint8List outPath;
|
||||||
|
final String advName;
|
||||||
|
final int lastAdvert; // Unix timestamp
|
||||||
|
final int advLat; // Latitude as int32
|
||||||
|
final int advLon; // Longitude as int32
|
||||||
|
final int lastMod; // Unix timestamp
|
||||||
|
|
||||||
|
// Telemetry data (updated separately)
|
||||||
|
ContactTelemetry? telemetry;
|
||||||
|
|
||||||
|
Contact({
|
||||||
|
required this.publicKey,
|
||||||
|
required this.type,
|
||||||
|
required this.flags,
|
||||||
|
required this.outPathLen,
|
||||||
|
required this.outPath,
|
||||||
|
required this.advName,
|
||||||
|
required this.lastAdvert,
|
||||||
|
required this.advLat,
|
||||||
|
required this.advLon,
|
||||||
|
required this.lastMod,
|
||||||
|
this.telemetry,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Get public key as hex string (first 8 bytes)
|
||||||
|
String get publicKeyShort {
|
||||||
|
if (publicKey.length < 8) return '';
|
||||||
|
return publicKey.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get full public key as hex string
|
||||||
|
String get publicKeyHex {
|
||||||
|
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert advLat/advLon to LatLng
|
||||||
|
LatLng? get advertLocation {
|
||||||
|
if (advLat == 0 && advLon == 0) return null;
|
||||||
|
// Convert from int32 to double (degrees)
|
||||||
|
final lat = advLat / 1e7;
|
||||||
|
final lon = advLon / 1e7;
|
||||||
|
return LatLng(lat, lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display location (prefer telemetry over advert)
|
||||||
|
LatLng? get displayLocation {
|
||||||
|
if (telemetry?.gpsLocation != null && telemetry!.isRecent) {
|
||||||
|
return telemetry!.gpsLocation;
|
||||||
|
}
|
||||||
|
return advertLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display battery (from telemetry or null)
|
||||||
|
double? get displayBattery {
|
||||||
|
return telemetry?.batteryPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if contact is a chat type (team member)
|
||||||
|
bool get isChat => type == ContactType.chat;
|
||||||
|
|
||||||
|
/// Check if contact is a repeater
|
||||||
|
bool get isRepeater => type == ContactType.repeater;
|
||||||
|
|
||||||
|
/// Check if contact is a room/channel
|
||||||
|
bool get isRoom => type == ContactType.room;
|
||||||
|
|
||||||
|
/// Get last seen time
|
||||||
|
DateTime get lastSeenTime {
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(lastAdvert * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get last modified time
|
||||||
|
DateTime get lastModifiedTime {
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(lastMod * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if contact was seen recently (within last 10 minutes)
|
||||||
|
bool get isRecentlySeen {
|
||||||
|
return DateTime.now().difference(lastSeenTime).inMinutes < 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get friendly time since last seen
|
||||||
|
String get timeSinceLastSeen {
|
||||||
|
final diff = DateTime.now().difference(lastSeenTime);
|
||||||
|
if (diff.inMinutes < 1) return 'Just now';
|
||||||
|
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
|
||||||
|
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||||
|
return '${diff.inDays}d ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact copyWith({
|
||||||
|
Uint8List? publicKey,
|
||||||
|
ContactType? type,
|
||||||
|
int? flags,
|
||||||
|
int? outPathLen,
|
||||||
|
Uint8List? outPath,
|
||||||
|
String? advName,
|
||||||
|
int? lastAdvert,
|
||||||
|
int? advLat,
|
||||||
|
int? advLon,
|
||||||
|
int? lastMod,
|
||||||
|
ContactTelemetry? telemetry,
|
||||||
|
}) {
|
||||||
|
return Contact(
|
||||||
|
publicKey: publicKey ?? this.publicKey,
|
||||||
|
type: type ?? this.type,
|
||||||
|
flags: flags ?? this.flags,
|
||||||
|
outPathLen: outPathLen ?? this.outPathLen,
|
||||||
|
outPath: outPath ?? this.outPath,
|
||||||
|
advName: advName ?? this.advName,
|
||||||
|
lastAdvert: lastAdvert ?? this.lastAdvert,
|
||||||
|
advLat: advLat ?? this.advLat,
|
||||||
|
advLon: advLon ?? this.advLon,
|
||||||
|
lastMod: lastMod ?? this.lastMod,
|
||||||
|
telemetry: telemetry ?? this.telemetry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Contact(name: $advName, type: ${type.displayName}, key: $publicKeyShort)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is Contact &&
|
||||||
|
publicKeyHex == other.publicKeyHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => publicKeyHex.hashCode;
|
||||||
|
}
|
||||||
76
lib/models/contact_telemetry.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
/// Contact telemetry data from MeshCore device
|
||||||
|
class ContactTelemetry {
|
||||||
|
final LatLng? gpsLocation;
|
||||||
|
final double? batteryPercentage;
|
||||||
|
final double? batteryMilliVolts;
|
||||||
|
final double? temperature;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
// Additional sensor data
|
||||||
|
final double? humidity;
|
||||||
|
final double? pressure;
|
||||||
|
final Map<String, dynamic>? extraSensorData;
|
||||||
|
|
||||||
|
ContactTelemetry({
|
||||||
|
this.gpsLocation,
|
||||||
|
this.batteryPercentage,
|
||||||
|
this.batteryMilliVolts,
|
||||||
|
this.temperature,
|
||||||
|
required this.timestamp,
|
||||||
|
this.humidity,
|
||||||
|
this.pressure,
|
||||||
|
this.extraSensorData,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Check if telemetry data is recent (within last 5 minutes)
|
||||||
|
bool get isRecent {
|
||||||
|
return DateTime.now().difference(timestamp).inMinutes < 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if battery level is low (< 20%)
|
||||||
|
bool get isLowBattery {
|
||||||
|
return batteryPercentage != null && batteryPercentage! < 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if battery level is critical (< 10%)
|
||||||
|
bool get isCriticalBattery {
|
||||||
|
return batteryPercentage != null && batteryPercentage! < 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get battery status color indicator
|
||||||
|
String get batteryStatus {
|
||||||
|
if (batteryPercentage == null) return 'unknown';
|
||||||
|
if (batteryPercentage! > 50) return 'good';
|
||||||
|
if (batteryPercentage! > 20) return 'medium';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactTelemetry copyWith({
|
||||||
|
LatLng? gpsLocation,
|
||||||
|
double? batteryPercentage,
|
||||||
|
double? batteryMilliVolts,
|
||||||
|
double? temperature,
|
||||||
|
DateTime? timestamp,
|
||||||
|
double? humidity,
|
||||||
|
double? pressure,
|
||||||
|
Map<String, dynamic>? extraSensorData,
|
||||||
|
}) {
|
||||||
|
return ContactTelemetry(
|
||||||
|
gpsLocation: gpsLocation ?? this.gpsLocation,
|
||||||
|
batteryPercentage: batteryPercentage ?? this.batteryPercentage,
|
||||||
|
batteryMilliVolts: batteryMilliVolts ?? this.batteryMilliVolts,
|
||||||
|
temperature: temperature ?? this.temperature,
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
humidity: humidity ?? this.humidity,
|
||||||
|
pressure: pressure ?? this.pressure,
|
||||||
|
extraSensorData: extraSensorData ?? this.extraSensorData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ContactTelemetry(gps: $gpsLocation, battery: $batteryPercentage%, temp: $temperature°C, time: $timestamp)';
|
||||||
|
}
|
||||||
|
}
|
||||||
173
lib/models/device_info.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
/// BLE connection state
|
||||||
|
enum ConnectionState {
|
||||||
|
disconnected,
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
disconnecting,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MeshCore device information
|
||||||
|
class DeviceInfo {
|
||||||
|
final String? deviceId;
|
||||||
|
final String? deviceName;
|
||||||
|
final ConnectionState connectionState;
|
||||||
|
final int? batteryMilliVolts;
|
||||||
|
final double? batteryPercentage;
|
||||||
|
final int? signalRssi;
|
||||||
|
final double? signalSnr;
|
||||||
|
final DateTime? lastUpdate;
|
||||||
|
|
||||||
|
// Self info from MeshCore device
|
||||||
|
final int? deviceType;
|
||||||
|
final int? txPower;
|
||||||
|
final int? maxTxPower;
|
||||||
|
final Uint8List? publicKey;
|
||||||
|
final int? advLat;
|
||||||
|
final int? advLon;
|
||||||
|
final bool? manualAddContacts;
|
||||||
|
final int? radioFreq;
|
||||||
|
final int? radioBw;
|
||||||
|
final int? radioSf;
|
||||||
|
final int? radioCr;
|
||||||
|
final String? selfName;
|
||||||
|
|
||||||
|
// Firmware info
|
||||||
|
final int? firmwareVersion;
|
||||||
|
final String? firmwareBuildDate;
|
||||||
|
final String? manufacturerModel;
|
||||||
|
|
||||||
|
DeviceInfo({
|
||||||
|
this.deviceId,
|
||||||
|
this.deviceName,
|
||||||
|
this.connectionState = ConnectionState.disconnected,
|
||||||
|
this.batteryMilliVolts,
|
||||||
|
this.batteryPercentage,
|
||||||
|
this.signalRssi,
|
||||||
|
this.signalSnr,
|
||||||
|
this.lastUpdate,
|
||||||
|
this.deviceType,
|
||||||
|
this.txPower,
|
||||||
|
this.maxTxPower,
|
||||||
|
this.publicKey,
|
||||||
|
this.advLat,
|
||||||
|
this.advLon,
|
||||||
|
this.manualAddContacts,
|
||||||
|
this.radioFreq,
|
||||||
|
this.radioBw,
|
||||||
|
this.radioSf,
|
||||||
|
this.radioCr,
|
||||||
|
this.selfName,
|
||||||
|
this.firmwareVersion,
|
||||||
|
this.firmwareBuildDate,
|
||||||
|
this.manufacturerModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Check if device is connected
|
||||||
|
bool get isConnected => connectionState == ConnectionState.connected;
|
||||||
|
|
||||||
|
/// Check if device is connecting
|
||||||
|
bool get isConnecting => connectionState == ConnectionState.connecting;
|
||||||
|
|
||||||
|
/// Check if device has error
|
||||||
|
bool get hasError => connectionState == ConnectionState.error;
|
||||||
|
|
||||||
|
/// Get battery percentage (calculated or provided)
|
||||||
|
double? get batteryPercent {
|
||||||
|
if (batteryPercentage != null) return batteryPercentage!;
|
||||||
|
if (batteryMilliVolts == null) return null;
|
||||||
|
|
||||||
|
// Rough conversion from mV to percentage (3.0V = 0%, 4.2V = 100%)
|
||||||
|
final voltage = batteryMilliVolts! / 1000.0;
|
||||||
|
if (voltage <= 3.0) return 0.0;
|
||||||
|
if (voltage >= 4.2) return 100.0;
|
||||||
|
return ((voltage - 3.0) / 1.2) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get battery status
|
||||||
|
String get batteryStatus {
|
||||||
|
final percent = batteryPercent;
|
||||||
|
if (percent == null) return 'Unknown';
|
||||||
|
if (percent > 80) return 'Excellent';
|
||||||
|
if (percent > 50) return 'Good';
|
||||||
|
if (percent > 20) return 'Low';
|
||||||
|
return 'Critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get signal strength category
|
||||||
|
String get signalStrength {
|
||||||
|
if (signalRssi == null) return 'Unknown';
|
||||||
|
if (signalRssi! > -60) return 'Excellent';
|
||||||
|
if (signalRssi! > -70) return 'Good';
|
||||||
|
if (signalRssi! > -80) return 'Fair';
|
||||||
|
return 'Poor';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get public key as hex string (short)
|
||||||
|
String? get publicKeyShort {
|
||||||
|
if (publicKey == null || publicKey!.length < 8) return null;
|
||||||
|
return publicKey!
|
||||||
|
.sublist(0, 8)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInfo copyWith({
|
||||||
|
String? deviceId,
|
||||||
|
String? deviceName,
|
||||||
|
ConnectionState? connectionState,
|
||||||
|
int? batteryMilliVolts,
|
||||||
|
double? batteryPercentage,
|
||||||
|
int? signalRssi,
|
||||||
|
double? signalSnr,
|
||||||
|
DateTime? lastUpdate,
|
||||||
|
int? deviceType,
|
||||||
|
int? txPower,
|
||||||
|
int? maxTxPower,
|
||||||
|
Uint8List? publicKey,
|
||||||
|
int? advLat,
|
||||||
|
int? advLon,
|
||||||
|
bool? manualAddContacts,
|
||||||
|
int? radioFreq,
|
||||||
|
int? radioBw,
|
||||||
|
int? radioSf,
|
||||||
|
int? radioCr,
|
||||||
|
String? selfName,
|
||||||
|
int? firmwareVersion,
|
||||||
|
String? firmwareBuildDate,
|
||||||
|
String? manufacturerModel,
|
||||||
|
}) {
|
||||||
|
return DeviceInfo(
|
||||||
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
deviceName: deviceName ?? this.deviceName,
|
||||||
|
connectionState: connectionState ?? this.connectionState,
|
||||||
|
batteryMilliVolts: batteryMilliVolts ?? this.batteryMilliVolts,
|
||||||
|
batteryPercentage: batteryPercentage ?? this.batteryPercentage,
|
||||||
|
signalRssi: signalRssi ?? this.signalRssi,
|
||||||
|
signalSnr: signalSnr ?? this.signalSnr,
|
||||||
|
lastUpdate: lastUpdate ?? this.lastUpdate,
|
||||||
|
deviceType: deviceType ?? this.deviceType,
|
||||||
|
txPower: txPower ?? this.txPower,
|
||||||
|
maxTxPower: maxTxPower ?? this.maxTxPower,
|
||||||
|
publicKey: publicKey ?? this.publicKey,
|
||||||
|
advLat: advLat ?? this.advLat,
|
||||||
|
advLon: advLon ?? this.advLon,
|
||||||
|
manualAddContacts: manualAddContacts ?? this.manualAddContacts,
|
||||||
|
radioFreq: radioFreq ?? this.radioFreq,
|
||||||
|
radioBw: radioBw ?? this.radioBw,
|
||||||
|
radioSf: radioSf ?? this.radioSf,
|
||||||
|
radioCr: radioCr ?? this.radioCr,
|
||||||
|
selfName: selfName ?? this.selfName,
|
||||||
|
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
|
||||||
|
firmwareBuildDate: firmwareBuildDate ?? this.firmwareBuildDate,
|
||||||
|
manufacturerModel: manufacturerModel ?? this.manufacturerModel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DeviceInfo(name: $deviceName, state: $connectionState, battery: ${batteryPercent?.toStringAsFixed(0)}%, signal: $signalRssi dBm)';
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/models/map_layer.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
enum MapLayerType {
|
||||||
|
openStreetMap,
|
||||||
|
openTopoMap,
|
||||||
|
esriWorldImagery,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLayer {
|
||||||
|
final MapLayerType type;
|
||||||
|
final String name;
|
||||||
|
final String urlTemplate;
|
||||||
|
final String attribution;
|
||||||
|
final int maxZoom;
|
||||||
|
|
||||||
|
const MapLayer({
|
||||||
|
required this.type,
|
||||||
|
required this.name,
|
||||||
|
required this.urlTemplate,
|
||||||
|
required this.attribution,
|
||||||
|
required this.maxZoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const openStreetMap = MapLayer(
|
||||||
|
type: MapLayerType.openStreetMap,
|
||||||
|
name: 'OpenStreetMap',
|
||||||
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 19,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const openTopoMap = MapLayer(
|
||||||
|
type: MapLayerType.openTopoMap,
|
||||||
|
name: 'OpenTopoMap',
|
||||||
|
urlTemplate: 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png',
|
||||||
|
attribution: '© OpenTopoMap (CC-BY-SA)',
|
||||||
|
maxZoom: 17,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const esriWorldImagery = MapLayer(
|
||||||
|
type: MapLayerType.esriWorldImagery,
|
||||||
|
name: 'ESRI Satellite',
|
||||||
|
urlTemplate:
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
|
attribution: '© Esri',
|
||||||
|
maxZoom: 19,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const List<MapLayer> allLayers = [
|
||||||
|
openStreetMap,
|
||||||
|
openTopoMap,
|
||||||
|
esriWorldImagery,
|
||||||
|
];
|
||||||
|
|
||||||
|
static MapLayer fromType(MapLayerType type) {
|
||||||
|
return allLayers.firstWhere((layer) => layer.type == type);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
lib/models/message.dart
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'sar_marker.dart';
|
||||||
|
|
||||||
|
/// Message text types from MeshCore protocol
|
||||||
|
enum MessageTextType {
|
||||||
|
plain(0),
|
||||||
|
cliData(1),
|
||||||
|
signedPlain(2);
|
||||||
|
|
||||||
|
const MessageTextType(this.value);
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
static MessageTextType fromValue(int value) {
|
||||||
|
return MessageTextType.values.firstWhere(
|
||||||
|
(e) => e.value == value,
|
||||||
|
orElse: () => MessageTextType.plain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Message type (contact or channel)
|
||||||
|
enum MessageType {
|
||||||
|
contact,
|
||||||
|
channel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MeshCore message model
|
||||||
|
class Message {
|
||||||
|
final String id;
|
||||||
|
final MessageType messageType;
|
||||||
|
final Uint8List? senderPublicKeyPrefix; // 6 bytes for contact messages
|
||||||
|
final int? channelIdx; // For channel messages
|
||||||
|
final int pathLen;
|
||||||
|
final MessageTextType textType;
|
||||||
|
final int senderTimestamp; // Unix timestamp
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
// SAR marker data (if this is a SAR message)
|
||||||
|
final bool isSarMarker;
|
||||||
|
final SarMarkerType? sarMarkerType;
|
||||||
|
final LatLng? sarGpsCoordinates;
|
||||||
|
|
||||||
|
// Display metadata
|
||||||
|
final DateTime receivedAt;
|
||||||
|
final String? senderName;
|
||||||
|
|
||||||
|
Message({
|
||||||
|
required this.id,
|
||||||
|
required this.messageType,
|
||||||
|
this.senderPublicKeyPrefix,
|
||||||
|
this.channelIdx,
|
||||||
|
required this.pathLen,
|
||||||
|
required this.textType,
|
||||||
|
required this.senderTimestamp,
|
||||||
|
required this.text,
|
||||||
|
this.isSarMarker = false,
|
||||||
|
this.sarMarkerType,
|
||||||
|
this.sarGpsCoordinates,
|
||||||
|
required this.receivedAt,
|
||||||
|
this.senderName,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Get sender public key as hex string
|
||||||
|
String? get senderKeyShort {
|
||||||
|
if (senderPublicKeyPrefix == null) return null;
|
||||||
|
return senderPublicKeyPrefix!
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get sender timestamp as DateTime
|
||||||
|
DateTime get sentAt {
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if message is from a channel
|
||||||
|
bool get isChannelMessage => messageType == MessageType.channel;
|
||||||
|
|
||||||
|
/// Check if message is from a contact
|
||||||
|
bool get isContactMessage => messageType == MessageType.contact;
|
||||||
|
|
||||||
|
/// Get friendly time since message was sent
|
||||||
|
String get timeAgo {
|
||||||
|
final diff = DateTime.now().difference(sentAt);
|
||||||
|
if (diff.inMinutes < 1) return 'Just now';
|
||||||
|
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
|
||||||
|
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||||
|
return '${diff.inDays}d ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display name for sender
|
||||||
|
String get displaySender {
|
||||||
|
if (senderName != null && senderName!.isNotEmpty) {
|
||||||
|
return senderName!;
|
||||||
|
}
|
||||||
|
if (senderKeyShort != null) {
|
||||||
|
return senderKeyShort!.substring(0, 8);
|
||||||
|
}
|
||||||
|
if (isChannelMessage && channelIdx != null) {
|
||||||
|
return 'Channel $channelIdx';
|
||||||
|
}
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to SAR marker if applicable
|
||||||
|
SarMarker? toSarMarker() {
|
||||||
|
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SarMarker(
|
||||||
|
id: id,
|
||||||
|
type: sarMarkerType!,
|
||||||
|
location: sarGpsCoordinates!,
|
||||||
|
timestamp: sentAt,
|
||||||
|
senderPublicKey: senderPublicKeyPrefix,
|
||||||
|
senderName: senderName,
|
||||||
|
notes: text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message copyWith({
|
||||||
|
String? id,
|
||||||
|
MessageType? messageType,
|
||||||
|
Uint8List? senderPublicKeyPrefix,
|
||||||
|
int? channelIdx,
|
||||||
|
int? pathLen,
|
||||||
|
MessageTextType? textType,
|
||||||
|
int? senderTimestamp,
|
||||||
|
String? text,
|
||||||
|
bool? isSarMarker,
|
||||||
|
SarMarkerType? sarMarkerType,
|
||||||
|
LatLng? sarGpsCoordinates,
|
||||||
|
DateTime? receivedAt,
|
||||||
|
String? senderName,
|
||||||
|
}) {
|
||||||
|
return Message(
|
||||||
|
id: id ?? this.id,
|
||||||
|
messageType: messageType ?? this.messageType,
|
||||||
|
senderPublicKeyPrefix: senderPublicKeyPrefix ?? this.senderPublicKeyPrefix,
|
||||||
|
channelIdx: channelIdx ?? this.channelIdx,
|
||||||
|
pathLen: pathLen ?? this.pathLen,
|
||||||
|
textType: textType ?? this.textType,
|
||||||
|
senderTimestamp: senderTimestamp ?? this.senderTimestamp,
|
||||||
|
text: text ?? this.text,
|
||||||
|
isSarMarker: isSarMarker ?? this.isSarMarker,
|
||||||
|
sarMarkerType: sarMarkerType ?? this.sarMarkerType,
|
||||||
|
sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates,
|
||||||
|
receivedAt: receivedAt ?? this.receivedAt,
|
||||||
|
senderName: senderName ?? this.senderName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (isSarMarker) {
|
||||||
|
return 'Message(SAR: ${sarMarkerType?.displayName}, from: $displaySender)';
|
||||||
|
}
|
||||||
|
return 'Message(from: $displaySender, text: ${text.length > 30 ? '${text.substring(0, 30)}...' : text})';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is Message && id == other.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
}
|
||||||
126
lib/models/sar_marker.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
/// SAR (Search & Rescue) marker types
|
||||||
|
enum SarMarkerType {
|
||||||
|
foundPerson('🧑', 'Found Person'),
|
||||||
|
fire('🔥', 'Fire'),
|
||||||
|
stagingArea('🏕️', 'Staging Area'),
|
||||||
|
unknown('❓', 'Unknown');
|
||||||
|
|
||||||
|
const SarMarkerType(this.emoji, this.displayName);
|
||||||
|
final String emoji;
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
|
static SarMarkerType fromEmoji(String emoji) {
|
||||||
|
switch (emoji) {
|
||||||
|
case '🧑':
|
||||||
|
case '👤':
|
||||||
|
return SarMarkerType.foundPerson;
|
||||||
|
case '🔥':
|
||||||
|
return SarMarkerType.fire;
|
||||||
|
case '🏕️':
|
||||||
|
case '⛺':
|
||||||
|
return SarMarkerType.stagingArea;
|
||||||
|
default:
|
||||||
|
return SarMarkerType.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get map marker color
|
||||||
|
String get markerColor {
|
||||||
|
switch (this) {
|
||||||
|
case SarMarkerType.foundPerson:
|
||||||
|
return '#4CAF50'; // Green
|
||||||
|
case SarMarkerType.fire:
|
||||||
|
return '#F44336'; // Red
|
||||||
|
case SarMarkerType.stagingArea:
|
||||||
|
return '#2196F3'; // Blue
|
||||||
|
default:
|
||||||
|
return '#9E9E9E'; // Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SAR marker from special messages
|
||||||
|
class SarMarker {
|
||||||
|
final String id;
|
||||||
|
final SarMarkerType type;
|
||||||
|
final LatLng location;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Uint8List? senderPublicKey;
|
||||||
|
final String? senderName;
|
||||||
|
final String? notes;
|
||||||
|
|
||||||
|
SarMarker({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
required this.location,
|
||||||
|
required this.timestamp,
|
||||||
|
this.senderPublicKey,
|
||||||
|
this.senderName,
|
||||||
|
this.notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Get sender public key as hex string (short)
|
||||||
|
String? get senderKeyShort {
|
||||||
|
if (senderPublicKey == null || senderPublicKey!.length < 8) return null;
|
||||||
|
return senderPublicKey!
|
||||||
|
.sublist(0, 8)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get friendly time since marker was created
|
||||||
|
String get timeAgo {
|
||||||
|
final diff = DateTime.now().difference(timestamp);
|
||||||
|
if (diff.inMinutes < 1) return 'Just now';
|
||||||
|
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
|
||||||
|
if (diff.inHours < 24) return '${diff.inHours}h ago';
|
||||||
|
return '${diff.inDays}d ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if marker is recent (within last hour)
|
||||||
|
bool get isRecent {
|
||||||
|
return DateTime.now().difference(timestamp).inHours < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display name
|
||||||
|
String get displayName {
|
||||||
|
return '${type.emoji} ${type.displayName}';
|
||||||
|
}
|
||||||
|
|
||||||
|
SarMarker copyWith({
|
||||||
|
String? id,
|
||||||
|
SarMarkerType? type,
|
||||||
|
LatLng? location,
|
||||||
|
DateTime? timestamp,
|
||||||
|
Uint8List? senderPublicKey,
|
||||||
|
String? senderName,
|
||||||
|
String? notes,
|
||||||
|
}) {
|
||||||
|
return SarMarker(
|
||||||
|
id: id ?? this.id,
|
||||||
|
type: type ?? this.type,
|
||||||
|
location: location ?? this.location,
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
senderPublicKey: senderPublicKey ?? this.senderPublicKey,
|
||||||
|
senderName: senderName ?? this.senderName,
|
||||||
|
notes: notes ?? this.notes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SarMarker(type: ${type.displayName}, location: $location, sender: $senderName, time: $timeAgo)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is SarMarker && id == other.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
}
|
||||||
121
lib/providers/app_provider.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'connection_provider.dart';
|
||||||
|
import 'contacts_provider.dart';
|
||||||
|
import 'messages_provider.dart';
|
||||||
|
import '../services/tile_cache_service.dart';
|
||||||
|
|
||||||
|
/// Main App Provider - coordinates all other providers
|
||||||
|
class AppProvider with ChangeNotifier {
|
||||||
|
final ConnectionProvider connectionProvider;
|
||||||
|
final ContactsProvider contactsProvider;
|
||||||
|
final MessagesProvider messagesProvider;
|
||||||
|
final TileCacheService tileCacheService;
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
|
AppProvider({
|
||||||
|
required this.connectionProvider,
|
||||||
|
required this.contactsProvider,
|
||||||
|
required this.messagesProvider,
|
||||||
|
required this.tileCacheService,
|
||||||
|
}) {
|
||||||
|
_setupCallbacks();
|
||||||
|
_initializeTileCache();
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize tile cache service
|
||||||
|
Future<void> _initializeTileCache() async {
|
||||||
|
try {
|
||||||
|
await tileCacheService.initialize();
|
||||||
|
debugPrint('Tile cache initialized');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error initializing tile cache: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup callbacks between providers
|
||||||
|
void _setupCallbacks() {
|
||||||
|
// When a contact is received from BLE
|
||||||
|
connectionProvider.onContactReceived = (contact) {
|
||||||
|
contactsProvider.addOrUpdateContact(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
// When all contacts are received
|
||||||
|
connectionProvider.onContactsComplete = (contacts) {
|
||||||
|
contactsProvider.addContacts(contacts);
|
||||||
|
debugPrint('Received ${contacts.length} contacts');
|
||||||
|
};
|
||||||
|
|
||||||
|
// When a message is received
|
||||||
|
connectionProvider.onMessageReceived = (message) {
|
||||||
|
messagesProvider.addMessage(message);
|
||||||
|
|
||||||
|
// Optionally update sender name from contacts
|
||||||
|
if (message.senderPublicKeyPrefix != null) {
|
||||||
|
final contact = contactsProvider
|
||||||
|
.findContactByKey(message.senderPublicKeyPrefix!);
|
||||||
|
if (contact != null) {
|
||||||
|
final updatedMessage = message.copyWith(senderName: contact.advName);
|
||||||
|
// Note: You might want to update the message in the list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When telemetry is received
|
||||||
|
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
|
||||||
|
contactsProvider.updateTelemetry(publicKey, lppData);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the app (load contacts, sync time, etc.)
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sync device time
|
||||||
|
await connectionProvider.syncDeviceTime();
|
||||||
|
|
||||||
|
// Load contacts
|
||||||
|
await connectionProvider.getContacts();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Initialization error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh data (contacts, messages)
|
||||||
|
Future<void> refresh() async {
|
||||||
|
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connectionProvider.getContacts();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Refresh error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all data
|
||||||
|
void clearAllData() {
|
||||||
|
contactsProvider.clearContacts();
|
||||||
|
messagesProvider.clearAll();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get app statistics
|
||||||
|
Map<String, dynamic> get statistics {
|
||||||
|
return {
|
||||||
|
'connection': {
|
||||||
|
'isConnected': connectionProvider.deviceInfo.isConnected,
|
||||||
|
'deviceName': connectionProvider.deviceInfo.deviceName,
|
||||||
|
'battery': connectionProvider.deviceInfo.batteryPercent,
|
||||||
|
},
|
||||||
|
'contacts': contactsProvider.contactCounts,
|
||||||
|
'messages': messagesProvider.messageStats,
|
||||||
|
'sarMarkers': messagesProvider.sarMarkerStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
240
lib/providers/connection_provider.dart
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
|
import '../models/device_info.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
import '../models/message.dart';
|
||||||
|
import '../services/meshcore_ble_service.dart';
|
||||||
|
import '../services/cayenne_lpp_parser.dart';
|
||||||
|
import '../utils/sar_message_parser.dart';
|
||||||
|
|
||||||
|
/// Connection Provider - manages MeshCore BLE connection
|
||||||
|
class ConnectionProvider with ChangeNotifier {
|
||||||
|
final MeshCoreBleService _bleService = MeshCoreBleService();
|
||||||
|
|
||||||
|
DeviceInfo _deviceInfo = DeviceInfo();
|
||||||
|
DeviceInfo get deviceInfo => _deviceInfo;
|
||||||
|
|
||||||
|
List<BluetoothDevice> _scannedDevices = [];
|
||||||
|
List<BluetoothDevice> get scannedDevices => _scannedDevices;
|
||||||
|
|
||||||
|
bool _isScanning = false;
|
||||||
|
bool get isScanning => _isScanning;
|
||||||
|
|
||||||
|
String? _error;
|
||||||
|
String? get error => _error;
|
||||||
|
|
||||||
|
// Callbacks for other providers
|
||||||
|
Function(Contact)? onContactReceived;
|
||||||
|
Function(List<Contact>)? onContactsComplete;
|
||||||
|
Function(Message)? onMessageReceived;
|
||||||
|
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
|
||||||
|
|
||||||
|
ConnectionProvider() {
|
||||||
|
_initializeBleService();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeBleService() {
|
||||||
|
_bleService.onConnectionStateChanged = (isConnected) {
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
connectionState: isConnected
|
||||||
|
? ConnectionState.connected
|
||||||
|
: ConnectionState.disconnected,
|
||||||
|
lastUpdate: DateTime.now(),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
};
|
||||||
|
|
||||||
|
_bleService.onError = (error) {
|
||||||
|
_error = error;
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
connectionState: ConnectionState.error,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
};
|
||||||
|
|
||||||
|
_bleService.onContactReceived = (contact) {
|
||||||
|
onContactReceived?.call(contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
_bleService.onContactsComplete = (contacts) {
|
||||||
|
onContactsComplete?.call(contacts);
|
||||||
|
};
|
||||||
|
|
||||||
|
_bleService.onMessageReceived = (message) {
|
||||||
|
// Parse SAR markers
|
||||||
|
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||||
|
onMessageReceived?.call(enhancedMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
_bleService.onTelemetryReceived = (publicKey, lppData) {
|
||||||
|
onTelemetryReceived?.call(publicKey, lppData);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start scanning for MeshCore devices
|
||||||
|
Future<void> startScan() async {
|
||||||
|
_isScanning = true;
|
||||||
|
_scannedDevices.clear();
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await for (final device
|
||||||
|
in _bleService.scanForDevices(timeout: const Duration(seconds: 10))) {
|
||||||
|
if (!_scannedDevices.any((d) => d.remoteId == device.remoteId)) {
|
||||||
|
_scannedDevices.add(device);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Scan error: $e';
|
||||||
|
} finally {
|
||||||
|
_isScanning = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop scanning
|
||||||
|
Future<void> stopScan() async {
|
||||||
|
await FlutterBluePlus.stopScan();
|
||||||
|
_isScanning = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to a device
|
||||||
|
Future<bool> connect(BluetoothDevice device) async {
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
deviceId: device.remoteId.toString(),
|
||||||
|
deviceName: device.platformName.isNotEmpty ? device.platformName : 'Unknown',
|
||||||
|
connectionState: ConnectionState.connecting,
|
||||||
|
);
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final success = await _bleService.connect(device);
|
||||||
|
if (!success) {
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
connectionState: ConnectionState.error,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from device
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
|
connectionState: ConnectionState.disconnecting,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
await _bleService.disconnect();
|
||||||
|
|
||||||
|
_deviceInfo = DeviceInfo(
|
||||||
|
connectionState: ConnectionState.disconnected,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get contacts from device
|
||||||
|
Future<void> getContacts() async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
_error = 'Not connected to device';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _bleService.getContacts();
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to get contacts: $e';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send text message to contact
|
||||||
|
Future<void> sendTextMessage({
|
||||||
|
required Uint8List contactPublicKey,
|
||||||
|
required String text,
|
||||||
|
}) async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
_error = 'Not connected to device';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _bleService.sendTextMessage(
|
||||||
|
contactPublicKey: contactPublicKey,
|
||||||
|
text: text,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to send message: $e';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send channel message
|
||||||
|
Future<void> sendChannelMessage({
|
||||||
|
required int channelIdx,
|
||||||
|
required String text,
|
||||||
|
}) async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
_error = 'Not connected to device';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _bleService.sendChannelMessage(
|
||||||
|
channelIdx: channelIdx,
|
||||||
|
text: text,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to send channel message: $e';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request telemetry from contact
|
||||||
|
Future<void> requestTelemetry(Uint8List contactPublicKey) async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
_error = 'Not connected to device';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _bleService.requestTelemetry(contactPublicKey);
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to request telemetry: $e';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set device time to current time
|
||||||
|
Future<void> syncDeviceTime() async {
|
||||||
|
if (!_bleService.isConnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _bleService.setDeviceTime();
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to sync time: $e';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear error message
|
||||||
|
void clearError() {
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bleService.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
135
lib/providers/contacts_provider.dart
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
import '../models/contact_telemetry.dart';
|
||||||
|
import '../services/cayenne_lpp_parser.dart';
|
||||||
|
|
||||||
|
/// Contacts Provider - manages contact list and telemetry
|
||||||
|
class ContactsProvider with ChangeNotifier {
|
||||||
|
final Map<String, Contact> _contacts = {};
|
||||||
|
|
||||||
|
List<Contact> get contacts => _contacts.values.toList();
|
||||||
|
|
||||||
|
List<Contact> get chatContacts =>
|
||||||
|
contacts.where((c) => c.isChat).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
|
List<Contact> get repeaters =>
|
||||||
|
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
|
List<Contact> get rooms =>
|
||||||
|
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
|
/// Get contacts with location (for map display)
|
||||||
|
List<Contact> get contactsWithLocation =>
|
||||||
|
contacts.where((c) => c.displayLocation != null).toList();
|
||||||
|
|
||||||
|
/// Get chat contacts with location (team members on map)
|
||||||
|
List<Contact> get chatContactsWithLocation =>
|
||||||
|
chatContacts.where((c) => c.displayLocation != null).toList();
|
||||||
|
|
||||||
|
/// Sort contacts by last seen (most recent first)
|
||||||
|
int _sortByLastSeen(Contact a, Contact b) {
|
||||||
|
return b.lastSeenTime.compareTo(a.lastSeenTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add or update a contact
|
||||||
|
void addOrUpdateContact(Contact contact) {
|
||||||
|
_contacts[contact.publicKeyHex] = contact;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple contacts
|
||||||
|
void addContacts(List<Contact> contacts) {
|
||||||
|
for (final contact in contacts) {
|
||||||
|
_contacts[contact.publicKeyHex] = contact;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update contact telemetry
|
||||||
|
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
||||||
|
// Find contact by public key prefix
|
||||||
|
final contact = _findContactByPrefix(publicKeyPrefix);
|
||||||
|
if (contact == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse Cayenne LPP data
|
||||||
|
final telemetry = CayenneLppParser.parse(lppData);
|
||||||
|
|
||||||
|
// Update contact with new telemetry
|
||||||
|
final updatedContact = contact.copyWith(telemetry: telemetry);
|
||||||
|
_contacts[contact.publicKeyHex] = updatedContact;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to parse telemetry: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find contact by public key prefix (6 bytes)
|
||||||
|
Contact? _findContactByPrefix(Uint8List prefix) {
|
||||||
|
if (prefix.length < 6) return null;
|
||||||
|
|
||||||
|
final prefixHex = prefix
|
||||||
|
.sublist(0, 6)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.publicKeyHex.startsWith(prefixHex)) {
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find contact by public key
|
||||||
|
Contact? findContactByKey(Uint8List publicKey) {
|
||||||
|
final keyHex =
|
||||||
|
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
return _contacts[keyHex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find contact by name
|
||||||
|
Contact? findContactByName(String name) {
|
||||||
|
return contacts.firstWhere(
|
||||||
|
(c) => c.advName == name,
|
||||||
|
orElse: () => contacts.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get contacts with low battery
|
||||||
|
List<Contact> get lowBatteryContacts {
|
||||||
|
return contacts.where((c) {
|
||||||
|
final battery = c.displayBattery;
|
||||||
|
return battery != null && battery < 20.0;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get recently seen contacts (within last 10 minutes)
|
||||||
|
List<Contact> get recentlySeenContacts {
|
||||||
|
return contacts.where((c) => c.isRecentlySeen).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all contacts
|
||||||
|
void clearContacts() {
|
||||||
|
_contacts.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a contact
|
||||||
|
void removeContact(String publicKeyHex) {
|
||||||
|
_contacts.remove(publicKeyHex);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get contact count by type
|
||||||
|
Map<String, int> get contactCounts {
|
||||||
|
return {
|
||||||
|
'chat': chatContacts.length,
|
||||||
|
'repeater': repeaters.length,
|
||||||
|
'room': rooms.length,
|
||||||
|
'total': contacts.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/providers/map_provider.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
class MapProvider with ChangeNotifier {
|
||||||
|
LatLng? _targetLocation;
|
||||||
|
double? _targetZoom;
|
||||||
|
bool _shouldAnimate = false;
|
||||||
|
|
||||||
|
LatLng? get targetLocation => _targetLocation;
|
||||||
|
double? get targetZoom => _targetZoom;
|
||||||
|
bool get shouldAnimate => _shouldAnimate;
|
||||||
|
|
||||||
|
void navigateToLocation({
|
||||||
|
required LatLng location,
|
||||||
|
double zoom = 15.0,
|
||||||
|
bool animate = true,
|
||||||
|
}) {
|
||||||
|
_targetLocation = location;
|
||||||
|
_targetZoom = zoom;
|
||||||
|
_shouldAnimate = animate;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearNavigation() {
|
||||||
|
_targetLocation = null;
|
||||||
|
_targetZoom = null;
|
||||||
|
_shouldAnimate = false;
|
||||||
|
// Don't notify listeners to avoid rebuilds
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateZoom(double zoom) {
|
||||||
|
_targetZoom = zoom;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
156
lib/providers/messages_provider.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../models/message.dart';
|
||||||
|
import '../models/sar_marker.dart';
|
||||||
|
|
||||||
|
/// Messages Provider - manages message history and SAR markers
|
||||||
|
class MessagesProvider with ChangeNotifier {
|
||||||
|
final List<Message> _messages = [];
|
||||||
|
final Map<String, SarMarker> _sarMarkers = {};
|
||||||
|
|
||||||
|
List<Message> get messages => List.unmodifiable(_messages);
|
||||||
|
|
||||||
|
List<Message> get contactMessages =>
|
||||||
|
_messages.where((m) => m.isContactMessage).toList();
|
||||||
|
|
||||||
|
List<Message> get channelMessages =>
|
||||||
|
_messages.where((m) => m.isChannelMessage).toList();
|
||||||
|
|
||||||
|
List<Message> get sarMarkerMessages =>
|
||||||
|
_messages.where((m) => m.isSarMarker).toList();
|
||||||
|
|
||||||
|
List<SarMarker> get sarMarkers => _sarMarkers.values.toList();
|
||||||
|
|
||||||
|
List<SarMarker> get foundPersonMarkers =>
|
||||||
|
sarMarkers.where((m) => m.type == SarMarkerType.foundPerson).toList();
|
||||||
|
|
||||||
|
List<SarMarker> get fireMarkers =>
|
||||||
|
sarMarkers.where((m) => m.type == SarMarkerType.fire).toList();
|
||||||
|
|
||||||
|
List<SarMarker> get stagingAreaMarkers =>
|
||||||
|
sarMarkers.where((m) => m.type == SarMarkerType.stagingArea).toList();
|
||||||
|
|
||||||
|
/// Add a message
|
||||||
|
void addMessage(Message message) {
|
||||||
|
_messages.add(message);
|
||||||
|
|
||||||
|
// If it's a SAR marker message, extract and store the marker
|
||||||
|
if (message.isSarMarker) {
|
||||||
|
final marker = message.toSarMarker();
|
||||||
|
if (marker != null) {
|
||||||
|
_sarMarkers[marker.id] = marker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple messages
|
||||||
|
void addMessages(List<Message> messages) {
|
||||||
|
for (final message in messages) {
|
||||||
|
_messages.add(message);
|
||||||
|
|
||||||
|
if (message.isSarMarker) {
|
||||||
|
final marker = message.toSarMarker();
|
||||||
|
if (marker != null) {
|
||||||
|
_sarMarkers[marker.id] = marker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get messages for a specific contact
|
||||||
|
List<Message> getMessagesForContact(String senderKeyShort) {
|
||||||
|
return _messages
|
||||||
|
.where((m) =>
|
||||||
|
m.isContactMessage &&
|
||||||
|
m.senderKeyShort != null &&
|
||||||
|
m.senderKeyShort!.startsWith(senderKeyShort))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get messages for a specific channel
|
||||||
|
List<Message> getMessagesForChannel(int channelIdx) {
|
||||||
|
return _messages
|
||||||
|
.where((m) => m.isChannelMessage && m.channelIdx == channelIdx)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get recent messages (last N messages)
|
||||||
|
List<Message> getRecentMessages({int count = 50}) {
|
||||||
|
final sorted = List<Message>.from(_messages)
|
||||||
|
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
|
||||||
|
return sorted.take(count).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get messages from last N hours
|
||||||
|
List<Message> getMessagesSince(Duration duration) {
|
||||||
|
final cutoff = DateTime.now().subtract(duration);
|
||||||
|
return _messages.where((m) => m.sentAt.isAfter(cutoff)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search messages by text
|
||||||
|
List<Message> searchMessages(String query) {
|
||||||
|
if (query.isEmpty) return [];
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
return _messages
|
||||||
|
.where((m) => m.text.toLowerCase().contains(lowerQuery))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get SAR marker by ID
|
||||||
|
SarMarker? getSarMarker(String id) {
|
||||||
|
return _sarMarkers[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get recent SAR markers (within last hour)
|
||||||
|
List<SarMarker> getRecentSarMarkers() {
|
||||||
|
return sarMarkers.where((m) => m.isRecent).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a SAR marker
|
||||||
|
void removeSarMarker(String id) {
|
||||||
|
_sarMarkers.remove(id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all messages
|
||||||
|
void clearMessages() {
|
||||||
|
_messages.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all SAR markers
|
||||||
|
void clearSarMarkers() {
|
||||||
|
_sarMarkers.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all data
|
||||||
|
void clearAll() {
|
||||||
|
_messages.clear();
|
||||||
|
_sarMarkers.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get message statistics
|
||||||
|
Map<String, int> get messageStats {
|
||||||
|
return {
|
||||||
|
'total': _messages.length,
|
||||||
|
'contact': contactMessages.length,
|
||||||
|
'channel': channelMessages.length,
|
||||||
|
'sar': sarMarkerMessages.length,
|
||||||
|
'sarMarkers': sarMarkers.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get SAR marker statistics
|
||||||
|
Map<String, int> get sarMarkerStats {
|
||||||
|
return {
|
||||||
|
'total': sarMarkers.length,
|
||||||
|
'foundPerson': foundPersonMarkers.length,
|
||||||
|
'fire': fireMarkers.length,
|
||||||
|
'stagingArea': stagingAreaMarkers.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
351
lib/screens/contacts_tab.dart
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../providers/contacts_provider.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
class ContactsTab extends StatelessWidget {
|
||||||
|
const ContactsTab({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ContactsProvider>(
|
||||||
|
builder: (context, contactsProvider, child) {
|
||||||
|
final chatContacts = contactsProvider.chatContacts;
|
||||||
|
final repeaters = contactsProvider.repeaters;
|
||||||
|
final rooms = contactsProvider.rooms;
|
||||||
|
|
||||||
|
if (contactsProvider.contacts.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.contacts_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No contacts yet',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Connect to a device and refresh to load contacts',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
children: [
|
||||||
|
// Team Members (Chat contacts)
|
||||||
|
if (chatContacts.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: 'Team Members',
|
||||||
|
count: chatContacts.length,
|
||||||
|
icon: Icons.people,
|
||||||
|
),
|
||||||
|
...chatContacts.map((contact) => _ContactTile(contact: contact)),
|
||||||
|
const Divider(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Repeaters
|
||||||
|
if (repeaters.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: 'Repeaters',
|
||||||
|
count: repeaters.length,
|
||||||
|
icon: Icons.router,
|
||||||
|
),
|
||||||
|
...repeaters.map((contact) => _ContactTile(contact: contact)),
|
||||||
|
const Divider(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Rooms/Channels
|
||||||
|
if (rooms.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: 'Rooms/Channels',
|
||||||
|
count: rooms.length,
|
||||||
|
icon: Icons.tag,
|
||||||
|
),
|
||||||
|
...rooms.map((contact) => _ContactTile(contact: contact)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionHeader extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final int count;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const _SectionHeader({
|
||||||
|
required this.title,
|
||||||
|
required this.count,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
count.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactTile extends StatelessWidget {
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
const _ContactTile({required this.contact});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasTelemetry = contact.telemetry != null && contact.telemetry!.isRecent;
|
||||||
|
final battery = contact.displayBattery;
|
||||||
|
final location = contact.displayLocation;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _getTypeColor(contact.type),
|
||||||
|
child: Icon(
|
||||||
|
_getTypeIcon(contact.type),
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
contact.advName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Battery indicator
|
||||||
|
if (battery != null) ...[
|
||||||
|
Icon(
|
||||||
|
_getBatteryIcon(battery),
|
||||||
|
size: 16,
|
||||||
|
color: _getBatteryColor(battery),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${battery.round()}%',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Type and last seen
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getTypeColor(contact.type).withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
contact.type.displayName,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 12,
|
||||||
|
color: contact.isRecentlySeen ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
contact.timeSinceLastSeen,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Telemetry info
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (hasTelemetry)
|
||||||
|
const Icon(Icons.sensors, size: 12, color: Colors.green)
|
||||||
|
else
|
||||||
|
const Icon(Icons.sensors_off, size: 12, color: Colors.grey),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
if (location != null)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'GPS: ${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'No GPS data',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.refresh, size: 20),
|
||||||
|
onPressed: () {
|
||||||
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
connectionProvider.requestTelemetry(contact.publicKey);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Requesting telemetry from ${contact.advName}'),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: 'Request telemetry',
|
||||||
|
),
|
||||||
|
onTap: () => _showContactDetails(context, contact),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContactDetails(BuildContext context, Contact contact) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(contact.advName),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_DetailRow('Type', contact.type.displayName),
|
||||||
|
_DetailRow('Public Key', contact.publicKeyShort),
|
||||||
|
_DetailRow('Last Seen', contact.timeSinceLastSeen),
|
||||||
|
const Divider(),
|
||||||
|
if (contact.displayLocation != null) ...[
|
||||||
|
const Text('Location:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
_DetailRow('Latitude', contact.displayLocation!.latitude.toStringAsFixed(6)),
|
||||||
|
_DetailRow('Longitude', contact.displayLocation!.longitude.toStringAsFixed(6)),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
if (contact.telemetry != null) ...[
|
||||||
|
const Text('Telemetry:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
if (contact.telemetry!.batteryPercentage != null)
|
||||||
|
_DetailRow('Battery', '${contact.telemetry!.batteryPercentage!.toStringAsFixed(1)}%'),
|
||||||
|
if (contact.telemetry!.temperature != null)
|
||||||
|
_DetailRow('Temperature', '${contact.telemetry!.temperature!.toStringAsFixed(1)}°C'),
|
||||||
|
_DetailRow('Updated', contact.telemetry!.isRecent ? 'Recently' : 'Stale'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _DetailRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Text(
|
||||||
|
'$label:',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getTypeIcon(ContactType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ContactType.chat:
|
||||||
|
return Icons.person;
|
||||||
|
case ContactType.repeater:
|
||||||
|
return Icons.router;
|
||||||
|
case ContactType.room:
|
||||||
|
return Icons.tag;
|
||||||
|
default:
|
||||||
|
return Icons.help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTypeColor(ContactType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ContactType.chat:
|
||||||
|
return Colors.blue;
|
||||||
|
case ContactType.repeater:
|
||||||
|
return Colors.green;
|
||||||
|
case ContactType.room:
|
||||||
|
return Colors.orange;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getBatteryIcon(double percentage) {
|
||||||
|
if (percentage > 80) return Icons.battery_full;
|
||||||
|
if (percentage > 50) return Icons.battery_5_bar;
|
||||||
|
if (percentage > 20) return Icons.battery_3_bar;
|
||||||
|
return Icons.battery_1_bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getBatteryColor(double percentage) {
|
||||||
|
if (percentage > 50) return Colors.green;
|
||||||
|
if (percentage > 20) return Colors.orange;
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
}
|
||||||
514
lib/screens/home_screen.dart
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
|
import '../providers/app_provider.dart';
|
||||||
|
import '../models/device_info.dart' as models;
|
||||||
|
import '../services/tile_cache_service.dart';
|
||||||
|
import 'messages_tab.dart';
|
||||||
|
import 'contacts_tab.dart';
|
||||||
|
import 'map_tab.dart';
|
||||||
|
import 'map_management_screen.dart';
|
||||||
|
|
||||||
|
class HomeScreen extends StatefulWidget {
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomeScreen> createState() => _HomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
|
_tabController.addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = _tabController.index;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showConnectionDialog(BuildContext context) {
|
||||||
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
|
||||||
|
// Start scanning immediately
|
||||||
|
connectionProvider.startScan();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) => Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.9,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF1E1E1E),
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
connectionProvider.stopScan();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'MeshCore',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Scanning for devices...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Info banner
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, color: Colors.white),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'The default pin for devices without a screen is 123456. Trouble pairing? Forget the bluetooth device in system settings.',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Device list
|
||||||
|
Expanded(
|
||||||
|
child: Consumer<ConnectionProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
if (provider.isScanning && provider.scannedDevices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.scannedDevices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'No devices found',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: provider.scannedDevices.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = provider.scannedDevices[index];
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF2D2D2D),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.bluetooth,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
device.platformName.isNotEmpty
|
||||||
|
? device.platformName
|
||||||
|
: 'Unknown Device',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Tap to connect',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await provider.connect(device);
|
||||||
|
if (context.mounted &&
|
||||||
|
provider.deviceInfo.isConnected) {
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
await appProvider.initialize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: _buildCompactStatusBar(),
|
||||||
|
actions: [
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.refresh),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Refresh Contacts'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
await appProvider.refresh();
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Refreshed contacts')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.map),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Map Management'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MapManagementScreen(
|
||||||
|
tileCacheService: appProvider.tileCacheService,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
MessagesTab(onNavigateToMap: () => _tabController.animateTo(2)),
|
||||||
|
const ContactsTab(),
|
||||||
|
const MapTab(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(icon: Icon(Icons.message), text: 'Messages'),
|
||||||
|
Tab(icon: Icon(Icons.contacts), text: 'Contacts'),
|
||||||
|
Tab(icon: Icon(Icons.map), text: 'Map'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompactStatusBar() {
|
||||||
|
return Consumer<ConnectionProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
final deviceInfo = provider.deviceInfo;
|
||||||
|
final isConnected = deviceInfo.isConnected;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'MeshCore',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
isConnected
|
||||||
|
? deviceInfo.deviceName ?? 'Connected'
|
||||||
|
: 'Disconnected',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isConnected)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _showConnectionDialog(context),
|
||||||
|
icon: const Icon(Icons.bluetooth, size: 18),
|
||||||
|
label: const Text('Connect'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await provider.disconnect();
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<AppProvider>().clearAllData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
side: const BorderSide(color: Colors.white),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Disconnect'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBar() {
|
||||||
|
return Consumer<ConnectionProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
final deviceInfo = provider.deviceInfo;
|
||||||
|
final isConnected = deviceInfo.isConnected;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Connection status
|
||||||
|
Icon(
|
||||||
|
isConnected ? Icons.bluetooth_connected : Icons.bluetooth_disabled,
|
||||||
|
color: isConnected ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
isConnected
|
||||||
|
? deviceInfo.deviceName ?? 'Connected'
|
||||||
|
: 'Not Connected',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Battery indicator
|
||||||
|
if (deviceInfo.batteryPercent != null) ...[
|
||||||
|
Icon(
|
||||||
|
_getBatteryIcon(deviceInfo.batteryPercent!),
|
||||||
|
color: _getBatteryColor(deviceInfo.batteryPercent!),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${deviceInfo.batteryPercent!.round()}%',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Signal strength
|
||||||
|
if (deviceInfo.signalRssi != null) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Icon(
|
||||||
|
Icons.signal_cellular_alt,
|
||||||
|
color: _getSignalColor(deviceInfo.signalRssi!),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${deviceInfo.signalRssi} dBm',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Connection buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: isConnected
|
||||||
|
? null
|
||||||
|
: () => _showConnectionDialog(context),
|
||||||
|
icon: const Icon(Icons.bluetooth_searching, size: 18),
|
||||||
|
label: const Text('Connect'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: !isConnected
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await provider.disconnect();
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<AppProvider>().clearAllData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.bluetooth_disabled, size: 18),
|
||||||
|
label: const Text('Disconnect'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isConnected) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
await appProvider.refresh();
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Refreshed contacts')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Error message
|
||||||
|
if (provider.error != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.red, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
provider.error!,
|
||||||
|
style: const TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
onPressed: provider.clearError,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getBatteryIcon(double percentage) {
|
||||||
|
if (percentage > 80) return Icons.battery_full;
|
||||||
|
if (percentage > 50) return Icons.battery_5_bar;
|
||||||
|
if (percentage > 20) return Icons.battery_3_bar;
|
||||||
|
return Icons.battery_1_bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getBatteryColor(double percentage) {
|
||||||
|
if (percentage > 50) return Colors.green;
|
||||||
|
if (percentage > 20) return Colors.orange;
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getSignalColor(int rssi) {
|
||||||
|
if (rssi > -60) return Colors.green;
|
||||||
|
if (rssi > -70) return Colors.orange;
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
}
|
||||||
728
lib/screens/map_management_screen.dart
Normal file
@@ -0,0 +1,728 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../services/tile_cache_service.dart';
|
||||||
|
import '../models/map_layer.dart';
|
||||||
|
|
||||||
|
class MapManagementScreen extends StatefulWidget {
|
||||||
|
final TileCacheService tileCacheService;
|
||||||
|
final MapLayer? initialLayer;
|
||||||
|
final LatLngBounds? initialBounds;
|
||||||
|
final int? initialZoom;
|
||||||
|
|
||||||
|
const MapManagementScreen({
|
||||||
|
super.key,
|
||||||
|
required this.tileCacheService,
|
||||||
|
this.initialLayer,
|
||||||
|
this.initialBounds,
|
||||||
|
this.initialZoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapManagementScreen> createState() => _MapManagementScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapManagementScreenState extends State<MapManagementScreen> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _statusMessage;
|
||||||
|
Map<String, dynamic>? _cacheStats;
|
||||||
|
|
||||||
|
// Download parameters
|
||||||
|
late MapLayer _selectedLayer;
|
||||||
|
late TextEditingController _northController;
|
||||||
|
late TextEditingController _southController;
|
||||||
|
late TextEditingController _eastController;
|
||||||
|
late TextEditingController _westController;
|
||||||
|
late int _minZoom;
|
||||||
|
late int _maxZoom;
|
||||||
|
double _downloadProgress = 0.0;
|
||||||
|
bool _isDownloading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// Initialize with provided values or defaults
|
||||||
|
_selectedLayer = widget.initialLayer ?? MapLayer.openStreetMap;
|
||||||
|
|
||||||
|
if (widget.initialBounds != null) {
|
||||||
|
_northController = TextEditingController(
|
||||||
|
text: widget.initialBounds!.north.toStringAsFixed(4),
|
||||||
|
);
|
||||||
|
_southController = TextEditingController(
|
||||||
|
text: widget.initialBounds!.south.toStringAsFixed(4),
|
||||||
|
);
|
||||||
|
_eastController = TextEditingController(
|
||||||
|
text: widget.initialBounds!.east.toStringAsFixed(4),
|
||||||
|
);
|
||||||
|
_westController = TextEditingController(
|
||||||
|
text: widget.initialBounds!.west.toStringAsFixed(4),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_northController = TextEditingController(text: '46.1');
|
||||||
|
_southController = TextEditingController(text: '46.0');
|
||||||
|
_eastController = TextEditingController(text: '14.6');
|
||||||
|
_westController = TextEditingController(text: '14.4');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set zoom levels
|
||||||
|
if (widget.initialZoom != null) {
|
||||||
|
_minZoom = (widget.initialZoom! - 2).clamp(1, 19);
|
||||||
|
_maxZoom = (widget.initialZoom! + 2).clamp(1, 19);
|
||||||
|
} else {
|
||||||
|
_minZoom = 10;
|
||||||
|
_maxZoom = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadCacheStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_northController.dispose();
|
||||||
|
_southController.dispose();
|
||||||
|
_eastController.dispose();
|
||||||
|
_westController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCacheStats() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final stats = await widget.tileCacheService.getStoreStats();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_cacheStats = stats;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = 'Error loading stats: $e';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadRegion() async {
|
||||||
|
try {
|
||||||
|
final north = double.tryParse(_northController.text);
|
||||||
|
final south = double.tryParse(_southController.text);
|
||||||
|
final east = double.tryParse(_eastController.text);
|
||||||
|
final west = double.tryParse(_westController.text);
|
||||||
|
|
||||||
|
if (north == null || south == null || east == null || west == null) {
|
||||||
|
_showError('Invalid coordinates. Please enter valid numbers.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (north <= south || east <= west) {
|
||||||
|
_showError('Invalid bounds. North must be > South, East must be > West.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bounds = LatLngBounds(
|
||||||
|
LatLng(south, west),
|
||||||
|
LatLng(north, east),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = true;
|
||||||
|
_downloadProgress = 0.0;
|
||||||
|
_statusMessage = 'Starting download...';
|
||||||
|
});
|
||||||
|
|
||||||
|
await widget.tileCacheService.downloadRegion(
|
||||||
|
layer: _selectedLayer,
|
||||||
|
bounds: bounds,
|
||||||
|
minZoom: _minZoom,
|
||||||
|
maxZoom: _maxZoom,
|
||||||
|
onProgress: (progress) {
|
||||||
|
print('UI received progress update: $progress%');
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress = progress;
|
||||||
|
_statusMessage = 'Downloading map tiles...';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = false;
|
||||||
|
_statusMessage = 'Download completed successfully!';
|
||||||
|
});
|
||||||
|
|
||||||
|
await _loadCacheStats();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Map download completed!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = false;
|
||||||
|
_statusMessage = 'Download failed: $e';
|
||||||
|
});
|
||||||
|
_showError('Download failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cancelDownload() async {
|
||||||
|
try {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _statusMessage = 'Cancelling download...');
|
||||||
|
|
||||||
|
await widget.tileCacheService.cancelDownload();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = false;
|
||||||
|
_statusMessage = 'Download cancelled';
|
||||||
|
});
|
||||||
|
|
||||||
|
await _loadCacheStats();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Download cancelled'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = false;
|
||||||
|
_statusMessage = 'Cancel failed: $e';
|
||||||
|
});
|
||||||
|
_showError('Cancel failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportMaps() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final exportPath = await widget.tileCacheService.exportCache();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(exportPath)],
|
||||||
|
subject: 'MeshCore SAR Maps Export',
|
||||||
|
text: 'Offline maps export from MeshCore SAR',
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Maps exported to: $exportPath'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
_showError('Export failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importMaps() async {
|
||||||
|
try {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['fmtc'],
|
||||||
|
allowMultiple: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
final filePath = result.files.first.path;
|
||||||
|
if (filePath == null) {
|
||||||
|
throw Exception('Invalid file path');
|
||||||
|
}
|
||||||
|
|
||||||
|
await widget.tileCacheService.importCache(filePath);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
await _loadCacheStats();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Maps imported successfully!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
_showError('Import failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearCache() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Clear Cache'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to delete all downloaded maps? This action cannot be undone.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
await widget.tileCacheService.clearCache();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
await _loadCacheStats();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Cache cleared successfully!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
_showError('Clear cache failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Map Management'),
|
||||||
|
),
|
||||||
|
body: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Cache Statistics
|
||||||
|
_buildStatisticsCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Download Region
|
||||||
|
_buildDownloadCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Import/Export/Clear
|
||||||
|
_buildActionsCard(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatisticsCard() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Cache Statistics',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: _loadCacheStats,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_cacheStats != null) ...[
|
||||||
|
_buildStatRow(
|
||||||
|
'Total Tiles',
|
||||||
|
'${_cacheStats!['tileCount'] ?? 0}',
|
||||||
|
Icons.grid_on,
|
||||||
|
),
|
||||||
|
_buildStatRow(
|
||||||
|
'Cache Size',
|
||||||
|
'${(_cacheStats!['sizeMB'] ?? 0.0).toStringAsFixed(2)} MB',
|
||||||
|
Icons.storage,
|
||||||
|
),
|
||||||
|
_buildStatRow(
|
||||||
|
'Store Name',
|
||||||
|
_cacheStats!['storeName'] ?? 'Unknown',
|
||||||
|
Icons.folder,
|
||||||
|
),
|
||||||
|
] else
|
||||||
|
const Text('No cache statistics available'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatRow(String label, String value, IconData icon) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(label, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
),
|
||||||
|
Text(value, style: TextStyle(color: Colors.grey[600])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDownloadCard() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Download Region',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Map Layer Selection
|
||||||
|
DropdownButtonFormField<MapLayer>(
|
||||||
|
value: _selectedLayer,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Map Layer',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: MapLayer.allLayers.map((layer) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: layer,
|
||||||
|
child: Text(layer.name),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: _isDownloading ? null : (layer) {
|
||||||
|
if (layer != null) {
|
||||||
|
setState(() => _selectedLayer = layer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Coordinates
|
||||||
|
Text(
|
||||||
|
'Region Bounds',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _northController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'North',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: '46.1',
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
enabled: !_isDownloading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _southController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'South',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: '46.0',
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
enabled: !_isDownloading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _eastController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'East',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: '14.6',
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
enabled: !_isDownloading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _westController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'West',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: '14.4',
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
enabled: !_isDownloading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Zoom Levels
|
||||||
|
Text(
|
||||||
|
'Zoom Levels',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Min: $_minZoom'),
|
||||||
|
Slider(
|
||||||
|
value: _minZoom.toDouble(),
|
||||||
|
min: 1,
|
||||||
|
max: 19,
|
||||||
|
divisions: 18,
|
||||||
|
label: '$_minZoom',
|
||||||
|
onChanged: _isDownloading ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_minZoom = value.toInt();
|
||||||
|
if (_minZoom > _maxZoom) {
|
||||||
|
_maxZoom = _minZoom;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Max: $_maxZoom'),
|
||||||
|
Slider(
|
||||||
|
value: _maxZoom.toDouble(),
|
||||||
|
min: 1,
|
||||||
|
max: 19,
|
||||||
|
divisions: 18,
|
||||||
|
label: '$_maxZoom',
|
||||||
|
onChanged: _isDownloading ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_maxZoom = value.toInt();
|
||||||
|
if (_maxZoom < _minZoom) {
|
||||||
|
_minZoom = _maxZoom;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Download Progress
|
||||||
|
if (_isDownloading) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_statusMessage ?? 'Downloading...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_downloadProgress.toStringAsFixed(1)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _downloadProgress / 100,
|
||||||
|
minHeight: 8,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Download/Cancel Button
|
||||||
|
if (_isDownloading)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _cancelDownload,
|
||||||
|
icon: const Icon(Icons.cancel),
|
||||||
|
label: const Text('Cancel Download'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _downloadRegion,
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: const Text('Download Region'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Note: Large regions or high zoom levels may take significant time and storage.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionsCard() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Map Actions',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Export Button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isDownloading ? null : _exportMaps,
|
||||||
|
icon: const Icon(Icons.upload),
|
||||||
|
label: const Text('Export Maps'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Import Button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isDownloading ? null : _importMaps,
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: const Text('Import Maps'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Clear Cache Button
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isDownloading ? null : _clearCache,
|
||||||
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
label: const Text('Clear All Maps'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
626
lib/screens/map_tab.dart
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import '../providers/contacts_provider.dart';
|
||||||
|
import '../providers/messages_provider.dart';
|
||||||
|
import '../providers/map_provider.dart';
|
||||||
|
import '../providers/app_provider.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
import '../models/sar_marker.dart';
|
||||||
|
import '../models/map_layer.dart';
|
||||||
|
import '../services/tile_cache_service.dart';
|
||||||
|
import '../widgets/map_markers.dart';
|
||||||
|
import 'map_management_screen.dart';
|
||||||
|
|
||||||
|
class MapTab extends StatefulWidget {
|
||||||
|
const MapTab({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapTab> createState() => _MapTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapTabState extends State<MapTab> {
|
||||||
|
final MapController _mapController = MapController();
|
||||||
|
final TileCacheService _tileCache = TileCacheService();
|
||||||
|
bool _isInitialized = false;
|
||||||
|
MapLayer _currentLayer = MapLayer.openStreetMap;
|
||||||
|
Position? _currentPosition;
|
||||||
|
bool _showLegend = true;
|
||||||
|
double _gpsUpdateDistance = 3.0; // meters
|
||||||
|
StreamSubscription<Position>? _positionStreamSubscription;
|
||||||
|
|
||||||
|
// Default center point (will be updated based on markers)
|
||||||
|
static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||||
|
static const double _defaultZoom = 13.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeTileCache();
|
||||||
|
_requestLocationPermission();
|
||||||
|
|
||||||
|
// Listen to map provider for navigation requests
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final mapProvider = context.read<MapProvider>();
|
||||||
|
mapProvider.addListener(_handleMapNavigation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestLocationPermission() async {
|
||||||
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!serviceEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission == LocationPermission.deniedForever) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial position
|
||||||
|
try {
|
||||||
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = position;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error getting location: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening to location updates
|
||||||
|
_positionStreamSubscription = Geolocator.getPositionStream(
|
||||||
|
locationSettings: LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: _gpsUpdateDistance.toInt(),
|
||||||
|
),
|
||||||
|
).listen((Position position) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = position;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleMapNavigation() {
|
||||||
|
final mapProvider = context.read<MapProvider>();
|
||||||
|
if (mapProvider.targetLocation != null && _isInitialized) {
|
||||||
|
_mapController.move(
|
||||||
|
mapProvider.targetLocation!,
|
||||||
|
mapProvider.targetZoom ?? _defaultZoom,
|
||||||
|
);
|
||||||
|
// Clear the navigation request after handling
|
||||||
|
mapProvider.clearNavigation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeTileCache() async {
|
||||||
|
try {
|
||||||
|
await _tileCache.initialize();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isInitialized = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error initializing tile cache: $e');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isInitialized = true; // Continue without caching
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final mapProvider = context.read<MapProvider>();
|
||||||
|
mapProvider.removeListener(_handleMapNavigation);
|
||||||
|
_positionStreamSubscription?.cancel();
|
||||||
|
_mapController.dispose();
|
||||||
|
_tileCache.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
|
||||||
|
final allPoints = <LatLng>[];
|
||||||
|
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.displayLocation != null) {
|
||||||
|
allPoints.add(contact.displayLocation!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final marker in sarMarkers) {
|
||||||
|
allPoints.add(marker.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPoints.isEmpty) return _defaultCenter;
|
||||||
|
|
||||||
|
double lat = 0, lng = 0;
|
||||||
|
for (final point in allPoints) {
|
||||||
|
lat += point.latitude;
|
||||||
|
lng += point.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LatLng(lat / allPoints.length, lng / allPoints.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showLayerSelector(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.layers),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Select Map Layer',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
tooltip: 'Download visible area',
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_navigateToDownload(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
...MapLayer.allLayers.map((layer) => ListTile(
|
||||||
|
leading: _currentLayer.type == layer.type
|
||||||
|
? const Icon(Icons.check_circle, color: Colors.green)
|
||||||
|
: const Icon(Icons.radio_button_unchecked),
|
||||||
|
title: Text(layer.name),
|
||||||
|
subtitle: Text(layer.attribution),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_currentLayer = layer;
|
||||||
|
});
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToDownload(BuildContext context) {
|
||||||
|
// Get current map bounds
|
||||||
|
final bounds = _mapController.camera.visibleBounds;
|
||||||
|
final currentZoom = _mapController.camera.zoom.round();
|
||||||
|
|
||||||
|
// Navigate to Map Management screen with pre-populated data
|
||||||
|
final appProvider = context.read<AppProvider>();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MapManagementScreen(
|
||||||
|
tileCacheService: appProvider.tileCacheService,
|
||||||
|
initialLayer: _currentLayer,
|
||||||
|
initialBounds: bounds,
|
||||||
|
initialZoom: currentZoom,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showOptionsMenu(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StatefulBuilder(
|
||||||
|
builder: (context, setModalState) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Map Options',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// Legend toggle
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.info_outline),
|
||||||
|
title: const Text('Show Legend'),
|
||||||
|
subtitle: const Text('Display marker type counts'),
|
||||||
|
value: _showLegend,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_showLegend = value;
|
||||||
|
});
|
||||||
|
setModalState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restartLocationStream() {
|
||||||
|
// Cancel existing subscription
|
||||||
|
_positionStreamSubscription?.cancel();
|
||||||
|
|
||||||
|
// Start new stream with updated distance
|
||||||
|
_positionStreamSubscription = Geolocator.getPositionStream(
|
||||||
|
locationSettings: LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: _gpsUpdateDistance.toInt(),
|
||||||
|
),
|
||||||
|
).listen((Position position) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = position;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer2<ContactsProvider, MessagesProvider>(
|
||||||
|
builder: (context, contactsProvider, messagesProvider, child) {
|
||||||
|
final contactsWithLocation = contactsProvider.chatContactsWithLocation;
|
||||||
|
final sarMarkers = messagesProvider.sarMarkers;
|
||||||
|
final center = _calculateCenter(contactsWithLocation, sarMarkers);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Map widget
|
||||||
|
_isInitialized
|
||||||
|
? FlutterMap(
|
||||||
|
mapController: _mapController,
|
||||||
|
options: MapOptions(
|
||||||
|
initialCenter: center,
|
||||||
|
initialZoom: _defaultZoom,
|
||||||
|
minZoom: 5,
|
||||||
|
maxZoom: 18,
|
||||||
|
interactionOptions: const InteractionOptions(
|
||||||
|
flags: InteractiveFlag.all,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: _currentLayer.urlTemplate,
|
||||||
|
tileProvider: _tileCache.getTileProvider(_currentLayer),
|
||||||
|
userAgentPackageName: 'com.meshcore.sar',
|
||||||
|
maxZoom: _currentLayer.maxZoom.toDouble(),
|
||||||
|
),
|
||||||
|
MarkerLayer(
|
||||||
|
markers: [
|
||||||
|
...MapMarkers.createTeamMemberMarkers(
|
||||||
|
contactsWithLocation,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
...MapMarkers.createSarMarkers(
|
||||||
|
sarMarkers,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
// User location marker
|
||||||
|
if (_currentPosition != null)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
_currentPosition!.latitude,
|
||||||
|
_currentPosition!.longitude,
|
||||||
|
),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.3),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.navigation,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Initializing map...',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Map legend overlay
|
||||||
|
if (_showLegend)
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: _MapLegend(
|
||||||
|
teamMemberCount: contactsWithLocation.length,
|
||||||
|
foundPersonCount: messagesProvider.foundPersonMarkers.length,
|
||||||
|
fireCount: messagesProvider.fireMarkers.length,
|
||||||
|
stagingAreaCount: messagesProvider.stagingAreaMarkers.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Map controls - right side
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FloatingActionButton.small(
|
||||||
|
heroTag: 'center_map',
|
||||||
|
onPressed: () async {
|
||||||
|
// Force update GPS location and jump to it
|
||||||
|
try {
|
||||||
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.best,
|
||||||
|
distanceFilter: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = position;
|
||||||
|
});
|
||||||
|
_mapController.move(
|
||||||
|
LatLng(position.latitude, position.longitude),
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error getting location: $e');
|
||||||
|
// Fallback to cached position or default center
|
||||||
|
if (_currentPosition != null) {
|
||||||
|
_mapController.move(
|
||||||
|
LatLng(
|
||||||
|
_currentPosition!.latitude,
|
||||||
|
_currentPosition!.longitude,
|
||||||
|
),
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_mapController.move(center, _defaultZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.my_location),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FloatingActionButton.small(
|
||||||
|
heroTag: 'layer_selector',
|
||||||
|
onPressed: () => _showLayerSelector(context),
|
||||||
|
child: const Icon(Icons.layers),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FloatingActionButton.small(
|
||||||
|
heroTag: 'options_menu',
|
||||||
|
onPressed: () => _showOptionsMenu(context),
|
||||||
|
child: const Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapLegend extends StatelessWidget {
|
||||||
|
final int teamMemberCount;
|
||||||
|
final int foundPersonCount;
|
||||||
|
final int fireCount;
|
||||||
|
final int stagingAreaCount;
|
||||||
|
|
||||||
|
const _MapLegend({
|
||||||
|
required this.teamMemberCount,
|
||||||
|
required this.foundPersonCount,
|
||||||
|
required this.fireCount,
|
||||||
|
required this.stagingAreaCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Legend',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_LegendItem(
|
||||||
|
icon: Icons.person,
|
||||||
|
color: Colors.blue,
|
||||||
|
label: 'Team',
|
||||||
|
count: teamMemberCount,
|
||||||
|
),
|
||||||
|
_LegendItem(
|
||||||
|
icon: Icons.person_pin,
|
||||||
|
color: Colors.green,
|
||||||
|
label: 'Found',
|
||||||
|
count: foundPersonCount,
|
||||||
|
),
|
||||||
|
_LegendItem(
|
||||||
|
icon: Icons.local_fire_department,
|
||||||
|
color: Colors.red,
|
||||||
|
label: 'Fire',
|
||||||
|
count: fireCount,
|
||||||
|
),
|
||||||
|
_LegendItem(
|
||||||
|
icon: Icons.home_work,
|
||||||
|
color: Colors.orange,
|
||||||
|
label: 'Staging',
|
||||||
|
count: stagingAreaCount,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LegendItem extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String label;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const _LegendItem({
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.label,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
count.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
205
lib/screens/messages_tab.dart
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../providers/messages_provider.dart';
|
||||||
|
import '../providers/contacts_provider.dart';
|
||||||
|
import '../providers/map_provider.dart';
|
||||||
|
import '../models/message.dart';
|
||||||
|
import '../utils/sar_message_parser.dart';
|
||||||
|
|
||||||
|
class MessagesTab extends StatelessWidget {
|
||||||
|
final VoidCallback onNavigateToMap;
|
||||||
|
|
||||||
|
const MessagesTab({super.key, required this.onNavigateToMap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<MessagesProvider>(
|
||||||
|
builder: (context, messagesProvider, child) {
|
||||||
|
final messages = messagesProvider.getRecentMessages(count: 100);
|
||||||
|
|
||||||
|
if (messages.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.message_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No messages yet',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Connect to a device to start receiving messages',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
itemCount: messages.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = messages[index];
|
||||||
|
return _MessageBubble(
|
||||||
|
message: message,
|
||||||
|
onTap: message.isSarMarker && message.sarGpsCoordinates != null
|
||||||
|
? () {
|
||||||
|
final mapProvider = context.read<MapProvider>();
|
||||||
|
mapProvider.navigateToLocation(
|
||||||
|
location: message.sarGpsCoordinates!,
|
||||||
|
zoom: 15.0,
|
||||||
|
);
|
||||||
|
onNavigateToMap();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageBubble extends StatelessWidget {
|
||||||
|
final Message message;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const _MessageBubble({
|
||||||
|
required this.message,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isSarMarker = message.isSarMarker;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSarMarker
|
||||||
|
? _getSarMarkerColor(context)
|
||||||
|
: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: isSarMarker
|
||||||
|
? Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header: Sender and time
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (message.isChannelMessage)
|
||||||
|
const Icon(Icons.tag, size: 16)
|
||||||
|
else
|
||||||
|
const Icon(Icons.person, size: 16),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
message.displaySender,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isSarMarker)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'SAR',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
message.timeAgo,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// SAR marker content
|
||||||
|
if (isSarMarker && message.sarMarkerType != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message.sarMarkerType!.emoji,
|
||||||
|
style: const TextStyle(fontSize: 32),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message.sarMarkerType!.displayName,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (message.sarGpsCoordinates != null)
|
||||||
|
Text(
|
||||||
|
'${message.sarGpsCoordinates!.latitude.toStringAsFixed(5)}, ${message.sarGpsCoordinates!.longitude.toStringAsFixed(5)}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Tap to view on map',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
// Regular message content
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
message.text,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getSarMarkerColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.primaryContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/services/buffer_reader.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// Buffer reader for parsing MeshCore protocol binary data
|
||||||
|
class BufferReader {
|
||||||
|
final Uint8List _buffer;
|
||||||
|
int _offset = 0;
|
||||||
|
|
||||||
|
BufferReader(this._buffer);
|
||||||
|
|
||||||
|
/// Get remaining bytes count
|
||||||
|
int get remainingBytesCount => _buffer.length - _offset;
|
||||||
|
|
||||||
|
/// Check if there are bytes remaining
|
||||||
|
bool get hasRemaining => _offset < _buffer.length;
|
||||||
|
|
||||||
|
/// Get current offset
|
||||||
|
int get offset => _offset;
|
||||||
|
|
||||||
|
/// Set offset
|
||||||
|
set offset(int value) => _offset = value;
|
||||||
|
|
||||||
|
/// Read a single byte (uint8)
|
||||||
|
int readByte() {
|
||||||
|
if (_offset >= _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||||
|
}
|
||||||
|
return _buffer[_offset++];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a signed byte (int8)
|
||||||
|
int readInt8() {
|
||||||
|
final value = readByte();
|
||||||
|
return value > 127 ? value - 256 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read unsigned 16-bit integer (little-endian)
|
||||||
|
int readUInt16LE() {
|
||||||
|
if (_offset + 2 > _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||||
|
}
|
||||||
|
final value = _buffer[_offset] | (_buffer[_offset + 1] << 8);
|
||||||
|
_offset += 2;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read signed 16-bit integer (little-endian)
|
||||||
|
int readInt16LE() {
|
||||||
|
final value = readUInt16LE();
|
||||||
|
return value > 32767 ? value - 65536 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read unsigned 32-bit integer (little-endian)
|
||||||
|
int readUInt32LE() {
|
||||||
|
if (_offset + 4 > _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||||
|
}
|
||||||
|
final value = _buffer[_offset] |
|
||||||
|
(_buffer[_offset + 1] << 8) |
|
||||||
|
(_buffer[_offset + 2] << 16) |
|
||||||
|
(_buffer[_offset + 3] << 24);
|
||||||
|
_offset += 4;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read signed 32-bit integer (little-endian)
|
||||||
|
int readInt32LE() {
|
||||||
|
final value = readUInt32LE();
|
||||||
|
return value > 2147483647 ? value - 4294967296 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a fixed number of bytes
|
||||||
|
Uint8List readBytes(int length) {
|
||||||
|
if (_offset + length > _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||||
|
}
|
||||||
|
final bytes = _buffer.sublist(_offset, _offset + length);
|
||||||
|
_offset += length;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read remaining bytes
|
||||||
|
Uint8List readRemainingBytes() {
|
||||||
|
final bytes = _buffer.sublist(_offset);
|
||||||
|
_offset = _buffer.length;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read null-terminated string (C-string) with max length
|
||||||
|
String readCString(int maxLength) {
|
||||||
|
if (_offset + maxLength > _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = _buffer.sublist(_offset, _offset + maxLength);
|
||||||
|
_offset += maxLength;
|
||||||
|
|
||||||
|
// Find null terminator
|
||||||
|
int nullIndex = bytes.indexOf(0);
|
||||||
|
if (nullIndex == -1) {
|
||||||
|
nullIndex = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode string up to null terminator
|
||||||
|
return utf8.decode(bytes.sublist(0, nullIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read length-prefixed string (remaining bytes as UTF-8)
|
||||||
|
String readString() {
|
||||||
|
final bytes = readRemainingBytes();
|
||||||
|
return utf8.decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peek at next byte without advancing offset
|
||||||
|
int peekByte() {
|
||||||
|
if (_offset >= _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to peek beyond buffer length');
|
||||||
|
}
|
||||||
|
return _buffer[_offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip bytes
|
||||||
|
void skip(int count) {
|
||||||
|
if (_offset + count > _buffer.length) {
|
||||||
|
throw Exception('Buffer overflow: attempting to skip beyond buffer length');
|
||||||
|
}
|
||||||
|
_offset += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset offset to beginning
|
||||||
|
void reset() {
|
||||||
|
_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BufferReader(length: ${_buffer.length}, offset: $_offset, remaining: $remainingBytesCount)';
|
||||||
|
}
|
||||||
|
}
|
||||||
129
lib/services/buffer_writer.dart
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// Buffer writer for creating MeshCore protocol binary data
|
||||||
|
class BufferWriter {
|
||||||
|
final List<int> _buffer = [];
|
||||||
|
|
||||||
|
/// Get current buffer length
|
||||||
|
int get length => _buffer.length;
|
||||||
|
|
||||||
|
/// Write a single byte (uint8)
|
||||||
|
void writeByte(int value) {
|
||||||
|
if (value < 0 || value > 255) {
|
||||||
|
throw ArgumentError('Byte value must be between 0 and 255');
|
||||||
|
}
|
||||||
|
_buffer.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a signed byte (int8)
|
||||||
|
void writeInt8(int value) {
|
||||||
|
if (value < -128 || value > 127) {
|
||||||
|
throw ArgumentError('Int8 value must be between -128 and 127');
|
||||||
|
}
|
||||||
|
_buffer.add(value < 0 ? value + 256 : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write unsigned 16-bit integer (little-endian)
|
||||||
|
void writeUInt16LE(int value) {
|
||||||
|
if (value < 0 || value > 65535) {
|
||||||
|
throw ArgumentError('UInt16 value must be between 0 and 65535');
|
||||||
|
}
|
||||||
|
_buffer.add(value & 0xFF);
|
||||||
|
_buffer.add((value >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write signed 16-bit integer (little-endian)
|
||||||
|
void writeInt16LE(int value) {
|
||||||
|
if (value < -32768 || value > 32767) {
|
||||||
|
throw ArgumentError('Int16 value must be between -32768 and 32767');
|
||||||
|
}
|
||||||
|
final unsigned = value < 0 ? value + 65536 : value;
|
||||||
|
writeUInt16LE(unsigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write unsigned 32-bit integer (little-endian)
|
||||||
|
void writeUInt32LE(int value) {
|
||||||
|
if (value < 0 || value > 4294967295) {
|
||||||
|
throw ArgumentError('UInt32 value must be between 0 and 4294967295');
|
||||||
|
}
|
||||||
|
_buffer.add(value & 0xFF);
|
||||||
|
_buffer.add((value >> 8) & 0xFF);
|
||||||
|
_buffer.add((value >> 16) & 0xFF);
|
||||||
|
_buffer.add((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write signed 32-bit integer (little-endian)
|
||||||
|
void writeInt32LE(int value) {
|
||||||
|
if (value < -2147483648 || value > 2147483647) {
|
||||||
|
throw ArgumentError('Int32 value must be between -2147483648 and 2147483647');
|
||||||
|
}
|
||||||
|
final unsigned = value < 0 ? value + 4294967296 : value;
|
||||||
|
writeUInt32LE(unsigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write bytes from Uint8List
|
||||||
|
void writeBytes(Uint8List bytes) {
|
||||||
|
_buffer.addAll(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write bytes from List<int>
|
||||||
|
void writeBytesFromList(List<int> bytes) {
|
||||||
|
_buffer.addAll(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write null-terminated string (C-string) with fixed length
|
||||||
|
/// Pads with zeros if string is shorter than maxLength
|
||||||
|
void writeCString(String str, int maxLength) {
|
||||||
|
final bytes = utf8.encode(str);
|
||||||
|
|
||||||
|
// Ensure we don't exceed max length
|
||||||
|
final length = bytes.length < maxLength ? bytes.length : maxLength;
|
||||||
|
|
||||||
|
// Write string bytes
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
_buffer.add(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad with zeros
|
||||||
|
for (int i = length; i < maxLength; i++) {
|
||||||
|
_buffer.add(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write length-prefixed string
|
||||||
|
void writeString(String str) {
|
||||||
|
final bytes = utf8.encode(str);
|
||||||
|
_buffer.addAll(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write string with length prefix (1 byte)
|
||||||
|
void writeLengthPrefixedString(String str) {
|
||||||
|
final bytes = utf8.encode(str);
|
||||||
|
if (bytes.length > 255) {
|
||||||
|
throw ArgumentError('String too long for length-prefixed format (max 255 bytes)');
|
||||||
|
}
|
||||||
|
writeByte(bytes.length);
|
||||||
|
_buffer.addAll(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get buffer as Uint8List
|
||||||
|
Uint8List toBytes() {
|
||||||
|
return Uint8List.fromList(_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the buffer
|
||||||
|
void clear() {
|
||||||
|
_buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get buffer as hex string (for debugging)
|
||||||
|
String toHexString() {
|
||||||
|
return _buffer.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BufferWriter(length: $length, hex: ${toHexString()})';
|
||||||
|
}
|
||||||
|
}
|
||||||