Files
meshcore-sar/lib/main.dart
T

341 lines
11 KiB
Dart

import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:geolocator/geolocator.dart';
import 'package:url_launcher/url_launcher.dart';
import 'providers/connection_provider.dart';
import 'providers/contacts_provider.dart';
import 'providers/messages_provider.dart';
import 'providers/map_provider.dart';
import 'providers/drawing_provider.dart';
import 'providers/channels_provider.dart';
import 'providers/voice_provider.dart';
import 'providers/image_provider.dart' as ip;
import 'providers/app_provider.dart';
import 'providers/sensors_provider.dart';
import 'services/voice_codec_service.dart';
import 'services/voice_player_service.dart';
import 'services/notification_service.dart';
import 'services/locale_preferences.dart';
import 'services/update_checker_service.dart';
import 'services/wizard_preferences.dart';
import 'screens/home_screen.dart';
import 'screens/welcome_wizard_screen.dart';
import 'theme/app_theme.dart';
import 'l10n/app_localizations.dart';
void main() {
runApp(const MeshCoreSarApp());
}
class MeshCoreSarApp extends StatefulWidget {
const MeshCoreSarApp({super.key});
@override
State<MeshCoreSarApp> createState() => _MeshCoreSarAppState();
}
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
AppThemeMode _themeMode = AppThemeMode.system;
Locale? _locale;
bool _isInitialized = false;
bool _shouldShowPermissionDialog = false;
bool _wizardCompleted = true; // Will be updated in _initializeApp()
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
await _loadThemePreference();
await _loadLocalePreference();
// Check if welcome wizard has been completed
final wizardCompleted = await WizardPreferences.isWizardCompleted();
// Initialize notification service
await NotificationService().initialize();
// Set up notification tap handler for update notifications
NotificationService().onNotificationTapped = _handleNotificationTap;
// Check if we need to request location permissions
await _checkLocationPermissions();
// Check for app updates (Android only) - runs in background
// Shows notification if update is available
_checkForUpdates();
setState(() {
_wizardCompleted = wizardCompleted;
_isInitialized = true;
});
}
/// Handle notification tap
void _handleNotificationTap(String? payload) {
if (payload == null) return;
debugPrint('[Main] Notification tapped: $payload');
// Handle update notification tap
if (payload.startsWith('update:')) {
final downloadUrl = payload.substring(7); // Remove 'update:' prefix
_launchUpdateDownload(downloadUrl);
}
// SAR and message notifications handled by their respective providers
}
/// Launch update download URL
Future<void> _launchUpdateDownload(String downloadUrl) async {
try {
final url = Uri.parse(downloadUrl);
final canLaunch = await canLaunchUrl(url);
if (!canLaunch) {
debugPrint('[Main] Cannot open download URL: $downloadUrl');
return;
}
await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (e) {
debugPrint('[Main] Error launching download URL: $e');
}
}
Future<void> _checkLocationPermissions() async {
try {
final permission = await Geolocator.checkPermission();
// Show dialog if permission is denied or not determined
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
_shouldShowPermissionDialog = true;
}
} catch (e) {
debugPrint('Error checking location permissions: $e');
}
}
Future<void> _loadThemePreference() async {
final prefs = await SharedPreferences.getInstance();
final themeName = prefs.getString('theme_mode') ?? 'system';
setState(() {
_themeMode = AppTheme.themeFromString(themeName);
});
}
Future<void> _loadLocalePreference() async {
final locale = await LocalePreferences.getLocale();
setState(() {
_locale = locale;
});
}
void _handleThemeChanged(AppThemeMode mode) {
setState(() {
_themeMode = mode;
});
}
void _handleLocaleChanged(Locale? locale) {
setState(() {
_locale = locale;
});
}
void _handleWizardCompleted() {
setState(() {
_wizardCompleted = true;
});
}
/// Check for app updates on Android only
/// Shows notification if update is available
Future<void> _checkForUpdates() async {
// Only check for updates on Android
if (!Platform.isAndroid) {
debugPrint('[UpdateChecker] Skipping update check (not Android)');
return;
}
try {
debugPrint('[UpdateChecker] Starting update check...');
final updateInfo = await UpdateCheckerService().checkForUpdate();
if (!updateInfo.isAvailable) {
debugPrint('[UpdateChecker] No update available');
return;
}
if (updateInfo.downloadUrl == null) {
debugPrint('[UpdateChecker] Update available but no download URL');
return;
}
debugPrint('[UpdateChecker] Update available! Showing notification...');
// Show notification (will be visible after app is initialized)
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
await NotificationService().showUpdateNotification(
currentVersion: updateInfo.currentCommitHash,
latestVersion: updateInfo.latestCommitHash ?? 'unknown',
downloadUrl: updateInfo.downloadUrl!,
localizations: null, // Will use English fallback
);
}
});
} catch (e) {
debugPrint('[UpdateChecker] Error checking for updates: $e');
}
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return const MaterialApp(
home: Scaffold(body: Center(child: CircularProgressIndicator())),
);
}
return MultiProvider(
providers: [
// Core providers
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
ChangeNotifierProvider(
create: (_) {
// Initialize early to load persisted contacts for offline viewing
// Self-contact filtering will happen later when BLE connects
final provider = ContactsProvider();
provider.initializeEarly();
return provider;
},
),
ChangeNotifierProvider(
create: (_) {
final provider = MessagesProvider();
// Initialize messages provider asynchronously
provider.initialize();
return provider;
},
),
ChangeNotifierProvider(create: (_) => MapProvider()),
ChangeNotifierProvider(
create: (_) {
final provider = DrawingProvider();
// Initialize drawing provider asynchronously
provider.initialize();
return provider;
},
),
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
ChangeNotifierProvider(create: (_) => SensorsProvider()),
// Voice provider (packet reassembly + playback)
ChangeNotifierProvider(
create: (_) => VoiceProvider(
codec: VoiceCodecService(),
player: VoicePlayerService(),
),
),
// Image provider (fragment reassembly + outgoing session cache)
ChangeNotifierProvider(create: (_) => ip.ImageProvider()),
// App provider that coordinates everything
// VoiceProvider is read via context.read inside create since it's already registered above
ChangeNotifierProxyProvider5<
ConnectionProvider,
ContactsProvider,
MessagesProvider,
DrawingProvider,
ChannelsProvider,
AppProvider
>(
create: (context) => AppProvider(
connectionProvider: context.read<ConnectionProvider>(),
contactsProvider: context.read<ContactsProvider>(),
messagesProvider: context.read<MessagesProvider>(),
drawingProvider: context.read<DrawingProvider>(),
channelsProvider: context.read<ChannelsProvider>(),
voiceProvider: context.read<VoiceProvider>(),
imageProvider: context.read<ip.ImageProvider>(),
),
update:
(
context,
conn,
contacts,
messages,
drawings,
channels,
previous,
) =>
previous ??
AppProvider(
connectionProvider: conn,
contactsProvider: contacts,
messagesProvider: messages,
drawingProvider: drawings,
channelsProvider: channels,
voiceProvider: context.read<VoiceProvider>(),
imageProvider: context.read<ip.ImageProvider>(),
),
),
],
child: _buildMaterialApp(),
);
}
Widget _buildMaterialApp() {
return Builder(
builder: (context) {
final systemBrightness = MediaQuery.platformBrightnessOf(context);
final materialApp = MaterialApp(
key: ValueKey<String?>(
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
),
title: 'MeshCore SAR',
debugShowCheckedModeBanner: false,
theme: AppTheme.getTheme(_themeMode, systemBrightness),
locale: _locale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: LocalePreferences.supportedLocales,
home: _wizardCompleted
? HomeScreen(
onThemeChanged: _handleThemeChanged,
onLocaleChanged: _handleLocaleChanged,
currentTheme: _themeMode,
currentLocale: _locale,
shouldShowPermissionDialog: _shouldShowPermissionDialog,
)
: WelcomeWizardScreen(onCompleted: _handleWizardCompleted),
);
// Wrap in SafeArea for Android only to fix navigation bar overlap (API ≥36)
// iOS doesn't need SafeArea wrapping (causes extra black space at bottom)
if (Platform.isAndroid) {
return SafeArea(
left: false,
right: false,
top: false, // prevents black status bar background
child: materialApp,
);
}
return materialApp;
},
);
}
}