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()})';
|
||||
}
|
||||
}
|
||||