diff --git a/ios/Runner.app.dSYM.zip b/ios/Runner.app.dSYM.zip
index 85f0d26..725d8af 100644
Binary files a/ios/Runner.app.dSYM.zip and b/ios/Runner.app.dSYM.zip differ
diff --git a/ios/Runner.ipa b/ios/Runner.ipa
index 426027e..59aa160 100644
Binary files a/ios/Runner.ipa and b/ios/Runner.ipa differ
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 652a87c..b4986bd 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -489,7 +489,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -529,7 +529,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -545,7 +545,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -676,7 +676,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -699,7 +699,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index dd820cd..0e9a6e7 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 17
+ 18
LSRequiresIPhoneOS
UILaunchStoryboardName
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 78eccb9..6b66536 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1776,5 +1776,10 @@
"placeholders": {
"power": { "type": "int" }
}
+ },
+
+ "you": "You",
+ "@you": {
+ "description": "Label for the current user in message bubbles"
}
}
diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb
index 1d284f6..8370aa8 100644
--- a/lib/l10n/app_hr.arb
+++ b/lib/l10n/app_hr.arb
@@ -590,5 +590,7 @@
"spreadingFactor": "Faktor širenja",
"codingRate": "Omjer kodiranja",
"txPowerDbm": "TX snaga (dBm)",
- "maxPowerDbm": "Maks: {power} dBm"
+ "maxPowerDbm": "Maks: {power} dBm",
+
+ "you": "Ti"
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 2e69a9a..80dfa47 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -1898,6 +1898,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Max: {power} dBm'**
String maxPowerDbm(int power);
+
+ /// Label for the current user in message bubbles
+ ///
+ /// In en, this message translates to:
+ /// **'You'**
+ String get you;
}
class _AppLocalizationsDelegate
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 3632038..afc1933 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -1035,4 +1035,7 @@ class AppLocalizationsEn extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Max: $power dBm';
}
+
+ @override
+ String get you => 'You';
}
diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart
index 6d3639e..21ac909 100644
--- a/lib/l10n/app_localizations_hr.dart
+++ b/lib/l10n/app_localizations_hr.dart
@@ -1037,4 +1037,7 @@ class AppLocalizationsHr extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Maks: $power dBm';
}
+
+ @override
+ String get you => 'Ti';
}
diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart
index a0d3af9..73b103e 100644
--- a/lib/l10n/app_localizations_sl.dart
+++ b/lib/l10n/app_localizations_sl.dart
@@ -1037,4 +1037,7 @@ class AppLocalizationsSl extends AppLocalizations {
String maxPowerDbm(int power) {
return 'Največ: $power dBm';
}
+
+ @override
+ String get you => 'Ti';
}
diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb
index 7017897..f891f98 100644
--- a/lib/l10n/app_sl.arb
+++ b/lib/l10n/app_sl.arb
@@ -590,5 +590,7 @@
"spreadingFactor": "Faktor razširitve",
"codingRate": "Razmerje kodiranja",
"txPowerDbm": "Izhodna moč (dBm)",
- "maxPowerDbm": "Največ: {power} dBm"
+ "maxPowerDbm": "Največ: {power} dBm",
+
+ "you": "Ti"
}
diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart
index 2799ac9..0e2dee0 100644
--- a/lib/providers/app_provider.dart
+++ b/lib/providers/app_provider.dart
@@ -6,6 +6,7 @@ import 'contacts_provider.dart';
import 'messages_provider.dart';
import 'drawing_provider.dart';
import '../services/tile_cache_service.dart';
+import '../services/location_tracking_service.dart';
import '../models/contact.dart';
import '../utils/drawing_message_parser.dart';
@@ -16,6 +17,7 @@ class AppProvider with ChangeNotifier {
final MessagesProvider messagesProvider;
final DrawingProvider drawingProvider;
final TileCacheService tileCacheService;
+ final LocationTrackingService locationTrackingService = LocationTrackingService();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
@@ -29,6 +31,7 @@ class AppProvider with ChangeNotifier {
}) {
_setupCallbacks();
_initializeTileCache();
+ _initializeLocationTracking();
_isInitialized = true;
}
@@ -42,8 +45,39 @@ class AppProvider with ChangeNotifier {
}
}
+ /// Initialize location tracking service
+ Future _initializeLocationTracking() async {
+ try {
+ // Initialize location tracking with BLE service
+ await locationTrackingService.initialize(connectionProvider.bleService);
+
+ // Setup callbacks
+ locationTrackingService.onPositionUpdate = (position) {
+ debugPrint('📍 [AppProvider] Position updated: ${position.latitude}, ${position.longitude}');
+ };
+
+ locationTrackingService.onBroadcastSent = (position) {
+ debugPrint('📡 [AppProvider] Position broadcast to mesh network');
+ };
+
+ locationTrackingService.onError = (error) {
+ debugPrint('❌ [AppProvider] Location tracking error: $error');
+ };
+
+ locationTrackingService.onTrackingStateChanged = (isTracking) {
+ debugPrint('🔄 [AppProvider] Location tracking state: ${isTracking ? "started" : "stopped"}');
+ };
+
+ debugPrint('✅ [AppProvider] Location tracking service initialized');
+ } catch (e) {
+ debugPrint('❌ [AppProvider] Error initializing location tracking: $e');
+ }
+ }
+
/// Setup callbacks between providers
void _setupCallbacks() {
+ // Monitor connection state changes to start/stop location tracking
+ connectionProvider.addListener(_handleConnectionStateChange);
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
// Pass device public key to filter out our own contact
@@ -339,6 +373,46 @@ class AppProvider with ChangeNotifier {
}
}
+ /// Handle connection state changes to manage location tracking
+ void _handleConnectionStateChange() {
+ final isConnected = connectionProvider.deviceInfo.isConnected;
+ final wasTracking = locationTrackingService.isTracking;
+
+ if (isConnected && !wasTracking) {
+ // Connection established - start location tracking
+ debugPrint('🔵 [AppProvider] BLE connected - starting location tracking');
+ _startLocationTracking();
+ } else if (!isConnected && wasTracking) {
+ // Connection lost - stop location tracking
+ debugPrint('🔴 [AppProvider] BLE disconnected - stopping location tracking');
+ _stopLocationTracking();
+ }
+ }
+
+ /// Start location tracking
+ Future _startLocationTracking() async {
+ try {
+ final started = await locationTrackingService.startTracking();
+ if (started) {
+ debugPrint('✅ [AppProvider] Location tracking started successfully');
+ } else {
+ debugPrint('⚠️ [AppProvider] Failed to start location tracking');
+ }
+ } catch (e) {
+ debugPrint('❌ [AppProvider] Error starting location tracking: $e');
+ }
+ }
+
+ /// Stop location tracking
+ Future _stopLocationTracking() async {
+ try {
+ await locationTrackingService.stopTracking();
+ debugPrint('✅ [AppProvider] Location tracking stopped');
+ } catch (e) {
+ debugPrint('❌ [AppProvider] Error stopping location tracking: $e');
+ }
+ }
+
/// Clear all data
void clearAllData() {
contactsProvider.clearContacts();
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index cfab60c..5e8e05d 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:vibration/vibration.dart';
import '../providers/connection_provider.dart';
import '../providers/app_provider.dart';
import '../providers/messages_provider.dart';
@@ -765,7 +767,30 @@ class _HomeScreenState extends State
// CENTER: Broadcast button
const SizedBox(width: 8),
FilledButton(
- onPressed: () => _advertiseDevice(context),
+ onPressed: () async {
+ // iOS: Use haptic feedback (always works)
+ // Android: Try vibration package for better control
+ try {
+ if (Theme.of(context).platform == TargetPlatform.iOS) {
+ // iOS: Try multiple haptic types for reliability
+ await HapticFeedback.lightImpact();
+ await Future.delayed(const Duration(milliseconds: 50));
+ await HapticFeedback.lightImpact();
+ } else {
+ // Android vibration
+ if (await Vibration.hasVibrator() ?? false) {
+ await Vibration.vibrate(duration: 50);
+ } else {
+ await HapticFeedback.mediumImpact();
+ }
+ }
+ } catch (e) {
+ // Fallback if anything fails
+ print('Haptic feedback error: $e');
+ await HapticFeedback.vibrate();
+ }
+ _advertiseDevice(context);
+ },
style: FilledButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart
index 8ab9962..2d5f16b 100644
--- a/lib/screens/messages_tab.dart
+++ b/lib/screens/messages_tab.dart
@@ -714,7 +714,7 @@ class _MessageBubble extends StatelessWidget {
// Get rich display name (with emoji if available)
final displayName = isOwnMessage
- ? 'You'
+ ? AppLocalizations.of(context)!.you
: message.getRichDisplayName(senderContact);
// For sent direct messages, look up recipient contact
diff --git a/lib/services/location_tracking_service.dart b/lib/services/location_tracking_service.dart
index a9b175b..9ef523a 100644
--- a/lib/services/location_tracking_service.dart
+++ b/lib/services/location_tracking_service.dart
@@ -53,7 +53,7 @@ class LocationTrackingService {
double maxDistanceMeters = 100.0;
/// Minimum time interval in seconds between broadcasts
- int minTimeIntervalSeconds = 155;
+ int minTimeIntervalSeconds = 30;
/// GPS update distance filter for position stream
double gpsUpdateDistance = 10.0;
@@ -380,6 +380,18 @@ class LocationTrackingService {
return;
}
+ // CRITICAL: Enforce minimum time between broadcasts to prevent mesh flooding
+ // This protects the mesh network from being overwhelmed by position updates
+ if (_lastBroadcastTime != null) {
+ final timeSinceLastBroadcast = DateTime.now().difference(_lastBroadcastTime!);
+ if (timeSinceLastBroadcast.inSeconds < minTimeIntervalSeconds) {
+ debugPrint(
+ '⏸️ [LocationTracking] Skipping broadcast: only ${timeSinceLastBroadcast.inSeconds}s since last broadcast (minimum: ${minTimeIntervalSeconds}s)',
+ );
+ return;
+ }
+ }
+
try {
debugPrint('📤 [LocationTracking] Broadcasting position to mesh network');
@@ -389,10 +401,10 @@ class LocationTrackingService {
longitude: position.longitude,
);
- // Send advertisement to mesh network
+ // Send advertisement to mesh network (only ONE advert per position update)
await _bleService!.sendSelfAdvert(floodMode: true);
- // Update broadcast tracking
+ // Update broadcast tracking IMMEDIATELY to prevent duplicate broadcasts
_lastBroadcastPosition = position;
_lastBroadcastTime = DateTime.now();
@@ -402,6 +414,7 @@ class LocationTrackingService {
await prefs.setDouble(_prefKeyLastLon, position.longitude);
debugPrint('✅ [LocationTracking] Position broadcast successful');
+ debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
onBroadcastSent?.call(position);
} catch (e) {
debugPrint('❌ [LocationTracking] Failed to broadcast position: $e');
@@ -412,6 +425,9 @@ class LocationTrackingService {
/// Manually broadcast current location immediately
///
/// Useful for "Send Location Now" button functionality.
+ /// Note: Manual broadcasts bypass automatic throttling and can be sent anytime.
+ /// However, they still update the last broadcast time to maintain proper spacing
+ /// for subsequent automatic broadcasts.
Future broadcastLocationNow() async {
if (!_isInitialized || _bleService == null) {
onError?.call('Location tracking service not initialized');
@@ -431,7 +447,9 @@ class LocationTrackingService {
return false;
}
- // Broadcast regardless of thresholds
+ debugPrint('📤 [LocationTracking] Manual broadcast requested');
+
+ // Broadcast regardless of automatic throttling thresholds
await _bleService!.setAdvertLatLon(
latitude: position.latitude,
longitude: position.longitude,
@@ -439,11 +457,12 @@ class LocationTrackingService {
await _bleService!.sendSelfAdvert(floodMode: true);
- // Update broadcast tracking
+ // Update broadcast tracking to maintain proper spacing for automatic broadcasts
_lastBroadcastPosition = position;
_lastBroadcastTime = DateTime.now();
debugPrint('✅ [LocationTracking] Manual broadcast successful');
+ debugPrint(' Automatic broadcasts will resume after ${minTimeIntervalSeconds}s');
onBroadcastSent?.call(position);
return true;
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index dc9b76e..ad46b68 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
+import device_info_plus
import flutter_blue_plus_darwin
import flutter_local_notifications
import geolocator_apple
@@ -15,6 +16,7 @@ import share_plus
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index b002d22..b0e53fb 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -129,6 +129,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
+ device_info_plus:
+ dependency: transitive
+ description:
+ name: device_info_plus
+ sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.5.0"
+ device_info_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: device_info_plus_platform_interface
+ sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.3"
fake_async:
dependency: transitive
description:
@@ -946,6 +962,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
+ vibration:
+ dependency: "direct main"
+ description:
+ name: vibration
+ sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ vibration_platform_interface:
+ dependency: transitive
+ description:
+ name: vibration_platform_interface
+ sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.3"
vm_service:
dependency: transitive
description:
@@ -970,6 +1002,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.15.0"
+ win32_registry:
+ dependency: transitive
+ description:
+ name: win32_registry
+ sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
wkt_parser:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 5aa8a56..4840ee8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -76,6 +76,9 @@ dependencies:
# Notifications
flutter_local_notifications: ^18.0.1
+ # Vibration
+ vibration: ^2.0.0
+
dev_dependencies:
flutter_test:
sdk: flutter