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