diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0390e72..77d69fd 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,7 +8,8 @@ plugins { android { namespace = "fr.chaboissier.rtime" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + //ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7915f4d..80636d6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,4 +49,8 @@ + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a3c8f5a..65344d4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -47,5 +47,11 @@ NSLocationWhenInUseUsageDescription To locate the flights. + NSCameraUsageDescription + This app needs access to your camera to take photos for your drones/batteries. + NSPhotoLibraryUsageDescription + This app needs access to your photo gallery to select images for your drones/batteries. + NSMicrophoneUsageDescription + This app does not use the microphone. diff --git a/lib/db/db_helper.dart b/lib/db/db_helper.dart index f6237fc..ee06134 100644 --- a/lib/db/db_helper.dart +++ b/lib/db/db_helper.dart @@ -50,7 +50,7 @@ class DbHelper { id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, - volatege REAL NOT NULL, + voltage REAL NOT NULL, image_uuid TEXT ) '''); @@ -84,6 +84,16 @@ class DbHelper { return maps.map((e) => Drone.fromMap(e)).toList(); } + Future updateDrone(Drone drone) async { + final db = await database; + return await db.update( + "drones", + drone.toMap(), + where: "id = ?", + whereArgs: [drone.id], + ); + } + Future deleteDrone(int droneId) async { // TODO: Delete image final db = await database; @@ -103,6 +113,16 @@ class DbHelper { return maps.map((e) => Battery.fromMap(e)).toList(); } + Future updateBattery(Battery battery) async { + final db = await database; + return await db.update( + "batteries", + battery.toMap(), + where: "id = ?", + whereArgs: [battery.id], + ); + } + Future deleteBattery(int batteryId) async { // TODO: Delete image final db = await database; diff --git a/lib/images_manager.dart b/lib/images_manager.dart index 2723811..b134388 100644 --- a/lib/images_manager.dart +++ b/lib/images_manager.dart @@ -8,7 +8,6 @@ import 'package:path/path.dart' as path; import 'package:uuid/uuid.dart'; import 'package:image/image.dart' as img; import 'package:image_cropper/image_cropper.dart' as img_cropper; -import 'package:uuid/v5.dart'; class ImagesManager { static final ImagesManager instance = ImagesManager._internal(); @@ -32,10 +31,10 @@ class ImagesManager { final directory = Directory.fromUri(directoryUri); if (!await directory.exists()) { - log.info("Image directory does not yet extists. Creating it."); + log.info("Image directory does not yet exist. Creating it."); } - directory.create(recursive: false); + await directory.create(recursive: true); log.info("Image directory set up at '$directory'"); @@ -43,11 +42,9 @@ class ImagesManager { } Future createImage(ImageSource source) async { - // Get image from camera or not final XFile? ximage = await ImagePicker().pickImage(source: source); if (ximage == null) return null; - // Crop image final uuid = Uuid().v6(); final imageDir = await imageDirectory; final finalPath = path.join( @@ -60,10 +57,9 @@ class ImagesManager { ); await ximage.saveTo(tempPath); - img_cropper.CroppedFile? cropped = await img_cropper.ImageCropper() - .cropImage( + img_cropper.CroppedFile? cropped = await img_cropper.ImageCropper().cropImage( sourcePath: tempPath, - aspectRatio: img_cropper.CropAspectRatio(ratioX: 1.0, ratioY: 1.0), + aspectRatio: const img_cropper.CropAspectRatio(ratioX: 1.0, ratioY: 1.0), uiSettings: [ img_cropper.AndroidUiSettings( toolbarTitle: "Crop image", @@ -74,7 +70,10 @@ class ImagesManager { ], ); - if (cropped == null) return null; + if (cropped == null) { + await File(tempPath).delete(); + return null; + } await File(finalPath).writeAsBytes(await cropped.readAsBytes()); await File(tempPath).delete(); @@ -94,10 +93,37 @@ class ImagesManager { final imagePath = path.join(imageDir.path, "$imageUuid.jpg"); final file = File(imagePath); if (!await file.exists()) { - log.warning("Tried to load an image that does not extist: '$imagePath'."); + log.warning("Tried to load an image that does not exist: '$imagePath'."); return null; } return Image.file(file); } + + Future deleteImage(String imageUuid) async { + final imageDir = await imageDirectory; + if (!Uuid.isValidUUID(fromString: imageUuid)) { + log.warning( + "Tried to delete an image with an invalid UUID : '$imageUuid'.", + ); + return false; + } + + final imagePath = path.join(imageDir.path, "$imageUuid.jpg"); + final file = File(imagePath); + + if (await file.exists()) { + try { + await file.delete(); + log.info("Image with UUID '$imageUuid' deleted successfully."); + return true; + } catch (e) { + log.severe("Error deleting image with UUID '$imageUuid': $e"); + return false; + } + } else { + log.warning("Tried to delete an image that does not exist: '$imagePath'."); + return false; + } + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..b27a751 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,210 @@ +{ + "@@locale": "en", + "appTitle": "Rtime", + "@appTitle": { + "description": "The title of the application" + }, + "yourDrones": "Your Drones", + "addDrone": "Add Drone", + "yourBatteries": "Your Batteries", + "addBattery": "Add Battery", + "latestFlights": "Latest Flights", + "newFlight": "New Flight", + "detailsOfDrone": "Details of drone: {droneName}", + "@detailsOfDrone": { + "placeholders": { + "droneName": { + "type": "String", + "example": "Chimera7" + } + } + }, + "detailsOfBattery": "Details of battery: {batteryName}", + "@detailsOfBattery": { + "placeholders": { + "batteryName": { + "type": "String", + "example": "GNB 1300mAh" + } + } + }, + "detailsOfFlight": "Details of flight: {flightName}", + "@detailsOfFlight": { + "placeholders": { + "flightName": { + "type": "String", + "example": "Flight at the beach - 01/07/2025" + } + } + }, + "settingsTitle": "Settings", + "languageSetting": "Language", + "english": "English", + "french": "French", + "languageChangedTo": "Language changed to {languageName}", + "@languageChangedTo": { + "placeholders": { + "languageName": { + "type": "String" + } + } + }, + "chooseDrone": "Choose your Drone", + "selectDroneHint": "Select a drone", + "chooseBattery": "Choose your Battery", + "selectBatteryHint": "Select a battery", + "startFlight": "Start Flight", + "stopFlight": "Stop Flight", + "selectDroneBattery": "Please select a drone and a battery.", + "flightStarted": "Flight started!", + "flightStopped": "Flight stopped. Duration: {flightDuration}", + "@flightStopped": { + "placeholders": { + "flightDuration": { + "type": "String", + "example": "00:05:30" + } + } + }, + "recordFlightLocation": "Record flight location (GPS)", + "locationServicesDisabled": "Location services are disabled.", + "locationPermissionsDenied": "Location permissions are denied.", + "locationPermissionsDeniedForever": "Location permissions are permanently denied, we cannot request permissions.", + "locationObtained": "Location obtained: Lat {latitude}, Lng {longitude}", + "@locationObtained": { + "placeholders": { + "latitude": { + "type": "double" + }, + "longitude": { + "type": "double" + } + } + }, + "locationRequiredForFlight": "GPS location is required but could not be obtained. Please check permissions and try again.", + "obtainingLocation": "Obtaining location...", + "failedToGetLocation": "Failed to get location: {error}", + "@failedToGetLocation": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "currentCoordinates": "Current coordinates: Lat {latitude}, Lng {longitude}", + "@currentCoordinates": { + "placeholders": { + "latitude": { + "type": "double" + }, + "longitude": { + "type": "double" + } + } + }, + "errorLoadingDrones": "Error loading drones", + "errorLoadingBatteries": "Error loading batteries", + "noDronesYet": "No drones added yet. Tap the '+' card to add one!", + "noBatteriesYet": "No batteries added yet. Tap the '+' card to add one!", + "flightSavedSuccessfully": "Flight saved successfully!", + "failedToSaveFlight": "Failed to save flight: {error}", + "@failedToSaveFlight": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorLoadingFlights": "Error loading flights", + "noFlightsYet": "No flights recorded yet.", + "droneName": "Drone Name", + "pleaseEnterDroneName": "Please enter a drone name", + "imageUuidOptional": "Image UUID (Optional)", + "saveDrone": "Save Drone", + "droneAddedSuccessfully": "Drone added successfully!", + "failedToAddDrone": "Failed to add drone", + "batteryName": "Battery Name", + "batteryType": "Battery Type", + "batteryVoltage": "Voltage (V)", + "pleaseEnterBatteryName": "Please enter a battery name", + "pleaseEnterBatteryType": "Please enter a battery type", + "pleaseEnterBatteryVoltage": "Please enter battery voltage", + "pleaseEnterValidNumber": "Please enter a valid number", + "saveBattery": "Save Battery", + "batteryAddedSuccessfully": "Battery added successfully!", + "failedToAddBattery": "Failed to add battery", + "droneImage": "Drone Image", + "batteryImage": "Battery Image", + "takePhoto": "Take Photo", + "chooseFromGallery": "Choose from Gallery", + "imageSelected": "Image selected!", + "imageSelectionCancelled": "Image selection cancelled.", + "removeImage": "Remove Image", + "imageDeletedSuccessfully": "Image deleted successfully!", + "droneDetails": "Drone Details", + "editDrone": "Edit Drone", + "saveChanges": "Save Changes", + "droneUpdatedSuccessfully": "Drone updated successfully!", + "failedToUpdateDrone": "Failed to update drone", + "deleteDroneConfirmationTitle": "Delete Drone?", + "deleteDroneConfirmationMessage": "Are you sure you want to delete {droneName}? This action cannot be undone.", + "@deleteDroneConfirmationMessage": { + "placeholders": { + "droneName": { + "type": "String" + } + } + }, + "droneDeletedSuccessfully": "Drone deleted successfully!", + "failedToDeleteDrone": "Failed to delete drone", + "batteryDetails": "Battery Details", + "editBattery": "Edit Battery", + "batteryUpdatedSuccessfully": "Battery updated successfully!", + "failedToUpdateBattery": "Failed to update battery", + "deleteBatteryConfirmationTitle": "Delete Battery?", + "deleteBatteryConfirmationMessage": "Are you sure you want to delete {batteryName}? This action cannot be undone.", + "@deleteBatteryConfirmationMessage": { + "placeholders": { + "batteryName": { + "type": "String" + } + } + }, + "batteryDeletedSuccessfully": "Battery deleted successfully!", + "failedToDeleteBattery": "Failed to delete battery", + "cancel": "Cancel", + "delete": "Delete", + "flightDetails": "Flight Details", + "startTime": "Start Time", + "endTime": "End Time", + "duration": "Duration", + "flightLocation": "Flight Location", + "noLocationData": "No location data for this flight.", + "unknown": "Unknown", + "errorLoadingData": "Error loading data", + "drone": "Drone", + "battery": "Battery", + "themeSetting": "Theme", + "themeLight": "Light Theme", + "themeDark": "Dark Theme", + "themeSystem": "System Default", + "themeChangedTo": "Theme changed to {themeName}", + "@themeChangedTo": { + "placeholders": { + "themeName": { + "type": "String" + } + } + }, + "deleteFlightConfirmationTitle": "Delete Flight?", + "deleteFlightConfirmationMessage": "Are you sure you want to delete {flightName}? This action cannot be undone.", + "@deleteFlightConfirmationMessage": { + "placeholders": { + "flightName": { + "type": "String" + } + } + }, + "flightDeletedSuccessfully": "Flight deleted successfully!", + "failedToDeleteFlight": "Failed to delete flight" +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000..c34c281 --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,210 @@ +{ + "@@locale": "fr", + "appTitle": "Rtime", + "@appTitle": { + "description": "Le titre de l'application" + }, + "yourDrones": "Vos Drones", + "addDrone": "Ajouter un drone", + "yourBatteries": "Vos Batteries", + "addBattery": "Ajouter une batterie", + "latestFlights": "Derniers Vols", + "newFlight": "Nouveau Vol", + "detailsOfDrone": "Détails du drone : {droneName}", + "@detailsOfDrone": { + "placeholders": { + "droneName": { + "type": "String", + "example": "Chimera7" + } + } + }, + "detailsOfBattery": "Détails de la batterie : {batteryName}", + "@detailsOfBattery": { + "placeholders": { + "batteryName": { + "type": "String", + "example": "GNB 1300mAh" + } + } + }, + "detailsOfFlight": "Détails du vol : {flightName}", + "@detailsOfFlight": { + "placeholders": { + "flightName": { + "type": "String", + "example": "Vol à la plage - 01/07/2025" + } + } + }, + "settingsTitle": "Paramètres", + "languageSetting": "Langue", + "english": "Anglais", + "french": "Français", + "languageChangedTo": "Langue changée en {languageName}", + "@languageChangedTo": { + "placeholders": { + "languageName": { + "type": "String" + } + } + }, + "chooseDrone": "Choisissez votre Drone", + "selectDroneHint": "Sélectionnez un drone", + "chooseBattery": "Choisissez votre Batterie", + "selectBatteryHint": "Sélectionnez une batterie", + "startFlight": "Lancer le vol", + "stopFlight": "Arrêter le vol", + "selectDroneBattery": "Veuillez sélectionner un drone et une batterie.", + "flightStarted": "Vol démarré !", + "flightStopped": "Vol arrêté. Durée : {flightDuration}", + "@flightStopped": { + "placeholders": { + "flightDuration": { + "type": "String", + "example": "00:05:30" + } + } + }, + "recordFlightLocation": "Enregistrer la localisation du vol (GPS)", + "locationServicesDisabled": "Les services de localisation sont désactivés.", + "locationPermissionsDenied": "Les permissions de localisation sont refusées.", + "locationPermissionsDeniedForever": "Les permissions de localisation sont refusées de manière permanente, nous ne pouvons pas les demander.", + "locationObtained": "Localisation obtenue : Lat {latitude}, Lon {longitude}", + "@locationObtained": { + "placeholders": { + "latitude": { + "type": "double" + }, + "longitude": { + "type": "double" + } + } + }, + "locationRequiredForFlight": "La localisation GPS est requise mais n'a pu être obtenue. Veuillez vérifier les permissions et réessayer.", + "obtainingLocation": "Obtention de la localisation...", + "failedToGetLocation": "Échec de l'obtention de la localisation : {error}", + "@failedToGetLocation": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "currentCoordinates": "Coordonnées actuelles : Lat {latitude}, Lon {longitude}", + "@currentCoordinates": { + "placeholders": { + "latitude": { + "type": "double" + }, + "longitude": { + "type": "double" + } + } + }, + "errorLoadingDrones": "Erreur lors du chargement des drones", + "errorLoadingBatteries": "Erreur lors du chargement des batteries", + "noDronesYet": "Aucun drone ajouté pour le moment. Appuyez sur la carte '+' pour en ajouter un !", + "noBatteriesYet": "Aucune batterie ajoutée pour le moment. Appuyez sur la carte '+' pour en ajouter une !", + "flightSavedSuccessfully": "Vol enregistré avec succès !", + "failedToSaveFlight": "Échec de l'enregistrement du vol : {error}", + "@failedToSaveFlight": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorLoadingFlights": "Erreur lors du chargement des vols", + "noFlightsYet": "Aucun vol enregistré pour le moment.", + "droneName": "Nom du drone", + "pleaseEnterDroneName": "Veuillez entrer un nom de drone", + "imageUuidOptional": "UUID de l'image (Facultatif)", + "saveDrone": "Enregistrer le drone", + "droneAddedSuccessfully": "Drone ajouté avec succès !", + "failedToAddDrone": "Échec de l'ajout du drone", + "batteryName": "Nom de la batterie", + "batteryType": "Type de batterie", + "batteryVoltage": "Tension (V)", + "pleaseEnterBatteryName": "Veuillez entrer un nom de batterie", + "pleaseEnterBatteryType": "Veuillez entrer un type de batterie", + "pleaseEnterBatteryVoltage": "Veuillez entrer la tension de la batterie", + "pleaseEnterValidNumber": "Veuillez entrer un nombre valide", + "saveBattery": "Enregistrer la batterie", + "batteryAddedSuccessfully": "Batterie ajoutée avec succès !", + "failedToAddBattery": "Échec de l'ajout de la batterie", + "droneImage": "Image du drone", + "batteryImage": "Image de la batterie", + "takePhoto": "Prendre une photo", + "chooseFromGallery": "Choisir de la galerie", + "imageSelected": "Image sélectionnée !", + "imageSelectionCancelled": "Sélection d'image annulée.", + "removeImage": "Supprimer l'image", + "imageDeletedSuccessfully": "Image supprimée avec succès !", + "droneDetails": "Détails du drone", + "editDrone": "Modifier le drone", + "saveChanges": "Enregistrer les modifications", + "droneUpdatedSuccessfully": "Drone mis à jour avec succès !", + "failedToUpdateDrone": "Échec de la mise à jour du drone", + "deleteDroneConfirmationTitle": "Supprimer le drone ?", + "deleteDroneConfirmationMessage": "Êtes-vous sûr de vouloir supprimer {droneName} ? Cette action est irréversible.", + "@deleteDroneConfirmationMessage": { + "placeholders": { + "droneName": { + "type": "String" + } + } + }, + "droneDeletedSuccessfully": "Drone supprimé avec succès !", + "failedToDeleteDrone": "Échec de la suppression du drone", + "batteryDetails": "Détails de la batterie", + "editBattery": "Modifier la batterie", + "batteryUpdatedSuccessfully": "Batterie mise à jour avec succès !", + "failedToUpdateBattery": "Échec de la mise à jour de la batterie", + "deleteBatteryConfirmationTitle": "Supprimer la batterie ?", + "deleteBatteryConfirmationMessage": "Êtes-vous sûr de vouloir supprimer {batteryName} ? Cette action est irréversible.", + "@deleteBatteryConfirmationMessage": { + "placeholders": { + "batteryName": { + "type": "String" + } + } + }, + "batteryDeletedSuccessfully": "Batterie supprimée avec succès !", + "failedToDeleteBattery": "Échec de la suppression de la batterie", + "cancel": "Annuler", + "delete": "Supprimer", + "flightDetails": "Détails du vol", + "startTime": "Heure de début", + "endTime": "Heure de fin", + "duration": "Durée", + "flightLocation": "Localisation du vol", + "noLocationData": "Pas de données de localisation pour ce vol.", + "unknown": "Inconnu", + "errorLoadingData": "Erreur de chargement des données", + "drone": "Drone", + "battery": "Batterie", + "themeSetting": "Thème", + "themeLight": "Thème Clair", + "themeDark": "Thème Sombre", + "themeSystem": "Par défaut du système", + "themeChangedTo": "Thème changé en {themeName}", + "@themeChangedTo": { + "placeholders": { + "themeName": { + "type": "String" + } + } + }, + "deleteFlightConfirmationTitle": "Supprimer le vol ?", + "deleteFlightConfirmationMessage": "Êtes-vous sûr de vouloir supprimer {flightName} ? Cette action est irréversible.", + "@deleteFlightConfirmationMessage": { + "placeholders": { + "flightName": { + "type": "String" + } + } + }, + "flightDeletedSuccessfully": "Vol supprimé avec succès !", + "failedToDeleteFlight": "Échec de la suppression du vol" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..053ae1c --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,747 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_fr.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('fr') + ]; + + /// The title of the application + /// + /// In en, this message translates to: + /// **'Rtime'** + String get appTitle; + + /// No description provided for @yourDrones. + /// + /// In en, this message translates to: + /// **'Your Drones'** + String get yourDrones; + + /// No description provided for @addDrone. + /// + /// In en, this message translates to: + /// **'Add Drone'** + String get addDrone; + + /// No description provided for @yourBatteries. + /// + /// In en, this message translates to: + /// **'Your Batteries'** + String get yourBatteries; + + /// No description provided for @addBattery. + /// + /// In en, this message translates to: + /// **'Add Battery'** + String get addBattery; + + /// No description provided for @latestFlights. + /// + /// In en, this message translates to: + /// **'Latest Flights'** + String get latestFlights; + + /// No description provided for @newFlight. + /// + /// In en, this message translates to: + /// **'New Flight'** + String get newFlight; + + /// No description provided for @detailsOfDrone. + /// + /// In en, this message translates to: + /// **'Details of drone: {droneName}'** + String detailsOfDrone(String droneName); + + /// No description provided for @detailsOfBattery. + /// + /// In en, this message translates to: + /// **'Details of battery: {batteryName}'** + String detailsOfBattery(String batteryName); + + /// No description provided for @detailsOfFlight. + /// + /// In en, this message translates to: + /// **'Details of flight: {flightName}'** + String detailsOfFlight(String flightName); + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @languageSetting. + /// + /// In en, this message translates to: + /// **'Language'** + String get languageSetting; + + /// No description provided for @english. + /// + /// In en, this message translates to: + /// **'English'** + String get english; + + /// No description provided for @french. + /// + /// In en, this message translates to: + /// **'French'** + String get french; + + /// No description provided for @languageChangedTo. + /// + /// In en, this message translates to: + /// **'Language changed to {languageName}'** + String languageChangedTo(String languageName); + + /// No description provided for @chooseDrone. + /// + /// In en, this message translates to: + /// **'Choose your Drone'** + String get chooseDrone; + + /// No description provided for @selectDroneHint. + /// + /// In en, this message translates to: + /// **'Select a drone'** + String get selectDroneHint; + + /// No description provided for @chooseBattery. + /// + /// In en, this message translates to: + /// **'Choose your Battery'** + String get chooseBattery; + + /// No description provided for @selectBatteryHint. + /// + /// In en, this message translates to: + /// **'Select a battery'** + String get selectBatteryHint; + + /// No description provided for @startFlight. + /// + /// In en, this message translates to: + /// **'Start Flight'** + String get startFlight; + + /// No description provided for @stopFlight. + /// + /// In en, this message translates to: + /// **'Stop Flight'** + String get stopFlight; + + /// No description provided for @selectDroneBattery. + /// + /// In en, this message translates to: + /// **'Please select a drone and a battery.'** + String get selectDroneBattery; + + /// No description provided for @flightStarted. + /// + /// In en, this message translates to: + /// **'Flight started!'** + String get flightStarted; + + /// No description provided for @flightStopped. + /// + /// In en, this message translates to: + /// **'Flight stopped. Duration: {flightDuration}'** + String flightStopped(String flightDuration); + + /// No description provided for @recordFlightLocation. + /// + /// In en, this message translates to: + /// **'Record flight location (GPS)'** + String get recordFlightLocation; + + /// No description provided for @locationServicesDisabled. + /// + /// In en, this message translates to: + /// **'Location services are disabled.'** + String get locationServicesDisabled; + + /// No description provided for @locationPermissionsDenied. + /// + /// In en, this message translates to: + /// **'Location permissions are denied.'** + String get locationPermissionsDenied; + + /// No description provided for @locationPermissionsDeniedForever. + /// + /// In en, this message translates to: + /// **'Location permissions are permanently denied, we cannot request permissions.'** + String get locationPermissionsDeniedForever; + + /// No description provided for @locationObtained. + /// + /// In en, this message translates to: + /// **'Location obtained: Lat {latitude}, Lng {longitude}'** + String locationObtained(double latitude, double longitude); + + /// No description provided for @locationRequiredForFlight. + /// + /// In en, this message translates to: + /// **'GPS location is required but could not be obtained. Please check permissions and try again.'** + String get locationRequiredForFlight; + + /// No description provided for @obtainingLocation. + /// + /// In en, this message translates to: + /// **'Obtaining location...'** + String get obtainingLocation; + + /// No description provided for @failedToGetLocation. + /// + /// In en, this message translates to: + /// **'Failed to get location: {error}'** + String failedToGetLocation(String error); + + /// No description provided for @currentCoordinates. + /// + /// In en, this message translates to: + /// **'Current coordinates: Lat {latitude}, Lng {longitude}'** + String currentCoordinates(double latitude, double longitude); + + /// No description provided for @errorLoadingDrones. + /// + /// In en, this message translates to: + /// **'Error loading drones'** + String get errorLoadingDrones; + + /// No description provided for @errorLoadingBatteries. + /// + /// In en, this message translates to: + /// **'Error loading batteries'** + String get errorLoadingBatteries; + + /// No description provided for @noDronesYet. + /// + /// In en, this message translates to: + /// **'No drones added yet. Tap the \'+\' card to add one!'** + String get noDronesYet; + + /// No description provided for @noBatteriesYet. + /// + /// In en, this message translates to: + /// **'No batteries added yet. Tap the \'+\' card to add one!'** + String get noBatteriesYet; + + /// No description provided for @flightSavedSuccessfully. + /// + /// In en, this message translates to: + /// **'Flight saved successfully!'** + String get flightSavedSuccessfully; + + /// No description provided for @failedToSaveFlight. + /// + /// In en, this message translates to: + /// **'Failed to save flight: {error}'** + String failedToSaveFlight(String error); + + /// No description provided for @errorLoadingFlights. + /// + /// In en, this message translates to: + /// **'Error loading flights'** + String get errorLoadingFlights; + + /// No description provided for @noFlightsYet. + /// + /// In en, this message translates to: + /// **'No flights recorded yet.'** + String get noFlightsYet; + + /// No description provided for @droneName. + /// + /// In en, this message translates to: + /// **'Drone Name'** + String get droneName; + + /// No description provided for @pleaseEnterDroneName. + /// + /// In en, this message translates to: + /// **'Please enter a drone name'** + String get pleaseEnterDroneName; + + /// No description provided for @imageUuidOptional. + /// + /// In en, this message translates to: + /// **'Image UUID (Optional)'** + String get imageUuidOptional; + + /// No description provided for @saveDrone. + /// + /// In en, this message translates to: + /// **'Save Drone'** + String get saveDrone; + + /// No description provided for @droneAddedSuccessfully. + /// + /// In en, this message translates to: + /// **'Drone added successfully!'** + String get droneAddedSuccessfully; + + /// No description provided for @failedToAddDrone. + /// + /// In en, this message translates to: + /// **'Failed to add drone'** + String get failedToAddDrone; + + /// No description provided for @batteryName. + /// + /// In en, this message translates to: + /// **'Battery Name'** + String get batteryName; + + /// No description provided for @batteryType. + /// + /// In en, this message translates to: + /// **'Battery Type'** + String get batteryType; + + /// No description provided for @batteryVoltage. + /// + /// In en, this message translates to: + /// **'Voltage (V)'** + String get batteryVoltage; + + /// No description provided for @pleaseEnterBatteryName. + /// + /// In en, this message translates to: + /// **'Please enter a battery name'** + String get pleaseEnterBatteryName; + + /// No description provided for @pleaseEnterBatteryType. + /// + /// In en, this message translates to: + /// **'Please enter a battery type'** + String get pleaseEnterBatteryType; + + /// No description provided for @pleaseEnterBatteryVoltage. + /// + /// In en, this message translates to: + /// **'Please enter battery voltage'** + String get pleaseEnterBatteryVoltage; + + /// No description provided for @pleaseEnterValidNumber. + /// + /// In en, this message translates to: + /// **'Please enter a valid number'** + String get pleaseEnterValidNumber; + + /// No description provided for @saveBattery. + /// + /// In en, this message translates to: + /// **'Save Battery'** + String get saveBattery; + + /// No description provided for @batteryAddedSuccessfully. + /// + /// In en, this message translates to: + /// **'Battery added successfully!'** + String get batteryAddedSuccessfully; + + /// No description provided for @failedToAddBattery. + /// + /// In en, this message translates to: + /// **'Failed to add battery'** + String get failedToAddBattery; + + /// No description provided for @droneImage. + /// + /// In en, this message translates to: + /// **'Drone Image'** + String get droneImage; + + /// No description provided for @batteryImage. + /// + /// In en, this message translates to: + /// **'Battery Image'** + String get batteryImage; + + /// No description provided for @takePhoto. + /// + /// In en, this message translates to: + /// **'Take Photo'** + String get takePhoto; + + /// No description provided for @chooseFromGallery. + /// + /// In en, this message translates to: + /// **'Choose from Gallery'** + String get chooseFromGallery; + + /// No description provided for @imageSelected. + /// + /// In en, this message translates to: + /// **'Image selected!'** + String get imageSelected; + + /// No description provided for @imageSelectionCancelled. + /// + /// In en, this message translates to: + /// **'Image selection cancelled.'** + String get imageSelectionCancelled; + + /// No description provided for @removeImage. + /// + /// In en, this message translates to: + /// **'Remove Image'** + String get removeImage; + + /// No description provided for @imageDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Image deleted successfully!'** + String get imageDeletedSuccessfully; + + /// No description provided for @droneDetails. + /// + /// In en, this message translates to: + /// **'Drone Details'** + String get droneDetails; + + /// No description provided for @editDrone. + /// + /// In en, this message translates to: + /// **'Edit Drone'** + String get editDrone; + + /// No description provided for @saveChanges. + /// + /// In en, this message translates to: + /// **'Save Changes'** + String get saveChanges; + + /// No description provided for @droneUpdatedSuccessfully. + /// + /// In en, this message translates to: + /// **'Drone updated successfully!'** + String get droneUpdatedSuccessfully; + + /// No description provided for @failedToUpdateDrone. + /// + /// In en, this message translates to: + /// **'Failed to update drone'** + String get failedToUpdateDrone; + + /// No description provided for @deleteDroneConfirmationTitle. + /// + /// In en, this message translates to: + /// **'Delete Drone?'** + String get deleteDroneConfirmationTitle; + + /// No description provided for @deleteDroneConfirmationMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete {droneName}? This action cannot be undone.'** + String deleteDroneConfirmationMessage(String droneName); + + /// No description provided for @droneDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Drone deleted successfully!'** + String get droneDeletedSuccessfully; + + /// No description provided for @failedToDeleteDrone. + /// + /// In en, this message translates to: + /// **'Failed to delete drone'** + String get failedToDeleteDrone; + + /// No description provided for @batteryDetails. + /// + /// In en, this message translates to: + /// **'Battery Details'** + String get batteryDetails; + + /// No description provided for @editBattery. + /// + /// In en, this message translates to: + /// **'Edit Battery'** + String get editBattery; + + /// No description provided for @batteryUpdatedSuccessfully. + /// + /// In en, this message translates to: + /// **'Battery updated successfully!'** + String get batteryUpdatedSuccessfully; + + /// No description provided for @failedToUpdateBattery. + /// + /// In en, this message translates to: + /// **'Failed to update battery'** + String get failedToUpdateBattery; + + /// No description provided for @deleteBatteryConfirmationTitle. + /// + /// In en, this message translates to: + /// **'Delete Battery?'** + String get deleteBatteryConfirmationTitle; + + /// No description provided for @deleteBatteryConfirmationMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete {batteryName}? This action cannot be undone.'** + String deleteBatteryConfirmationMessage(String batteryName); + + /// No description provided for @batteryDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Battery deleted successfully!'** + String get batteryDeletedSuccessfully; + + /// No description provided for @failedToDeleteBattery. + /// + /// In en, this message translates to: + /// **'Failed to delete battery'** + String get failedToDeleteBattery; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @flightDetails. + /// + /// In en, this message translates to: + /// **'Flight Details'** + String get flightDetails; + + /// No description provided for @startTime. + /// + /// In en, this message translates to: + /// **'Start Time'** + String get startTime; + + /// No description provided for @endTime. + /// + /// In en, this message translates to: + /// **'End Time'** + String get endTime; + + /// No description provided for @duration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get duration; + + /// No description provided for @flightLocation. + /// + /// In en, this message translates to: + /// **'Flight Location'** + String get flightLocation; + + /// No description provided for @noLocationData. + /// + /// In en, this message translates to: + /// **'No location data for this flight.'** + String get noLocationData; + + /// No description provided for @unknown. + /// + /// In en, this message translates to: + /// **'Unknown'** + String get unknown; + + /// No description provided for @errorLoadingData. + /// + /// In en, this message translates to: + /// **'Error loading data'** + String get errorLoadingData; + + /// No description provided for @drone. + /// + /// In en, this message translates to: + /// **'Drone'** + String get drone; + + /// No description provided for @battery. + /// + /// In en, this message translates to: + /// **'Battery'** + String get battery; + + /// No description provided for @themeSetting. + /// + /// In en, this message translates to: + /// **'Theme'** + String get themeSetting; + + /// No description provided for @themeLight. + /// + /// In en, this message translates to: + /// **'Light Theme'** + String get themeLight; + + /// No description provided for @themeDark. + /// + /// In en, this message translates to: + /// **'Dark Theme'** + String get themeDark; + + /// No description provided for @themeSystem. + /// + /// In en, this message translates to: + /// **'System Default'** + String get themeSystem; + + /// No description provided for @themeChangedTo. + /// + /// In en, this message translates to: + /// **'Theme changed to {themeName}'** + String themeChangedTo(String themeName); + + /// No description provided for @deleteFlightConfirmationTitle. + /// + /// In en, this message translates to: + /// **'Delete Flight?'** + String get deleteFlightConfirmationTitle; + + /// No description provided for @deleteFlightConfirmationMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete {flightName}? This action cannot be undone.'** + String deleteFlightConfirmationMessage(String flightName); + + /// No description provided for @flightDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Flight deleted successfully!'** + String get flightDeletedSuccessfully; + + /// No description provided for @failedToDeleteFlight. + /// + /// In en, this message translates to: + /// **'Failed to delete flight'** + String get failedToDeleteFlight; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en', 'fr'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': return AppLocalizationsEn(); + case 'fr': return AppLocalizationsFr(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..808734b --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,345 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Rtime'; + + @override + String get yourDrones => 'Your Drones'; + + @override + String get addDrone => 'Add Drone'; + + @override + String get yourBatteries => 'Your Batteries'; + + @override + String get addBattery => 'Add Battery'; + + @override + String get latestFlights => 'Latest Flights'; + + @override + String get newFlight => 'New Flight'; + + @override + String detailsOfDrone(String droneName) { + return 'Details of drone: $droneName'; + } + + @override + String detailsOfBattery(String batteryName) { + return 'Details of battery: $batteryName'; + } + + @override + String detailsOfFlight(String flightName) { + return 'Details of flight: $flightName'; + } + + @override + String get settingsTitle => 'Settings'; + + @override + String get languageSetting => 'Language'; + + @override + String get english => 'English'; + + @override + String get french => 'French'; + + @override + String languageChangedTo(String languageName) { + return 'Language changed to $languageName'; + } + + @override + String get chooseDrone => 'Choose your Drone'; + + @override + String get selectDroneHint => 'Select a drone'; + + @override + String get chooseBattery => 'Choose your Battery'; + + @override + String get selectBatteryHint => 'Select a battery'; + + @override + String get startFlight => 'Start Flight'; + + @override + String get stopFlight => 'Stop Flight'; + + @override + String get selectDroneBattery => 'Please select a drone and a battery.'; + + @override + String get flightStarted => 'Flight started!'; + + @override + String flightStopped(String flightDuration) { + return 'Flight stopped. Duration: $flightDuration'; + } + + @override + String get recordFlightLocation => 'Record flight location (GPS)'; + + @override + String get locationServicesDisabled => 'Location services are disabled.'; + + @override + String get locationPermissionsDenied => 'Location permissions are denied.'; + + @override + String get locationPermissionsDeniedForever => 'Location permissions are permanently denied, we cannot request permissions.'; + + @override + String locationObtained(double latitude, double longitude) { + return 'Location obtained: Lat $latitude, Lng $longitude'; + } + + @override + String get locationRequiredForFlight => 'GPS location is required but could not be obtained. Please check permissions and try again.'; + + @override + String get obtainingLocation => 'Obtaining location...'; + + @override + String failedToGetLocation(String error) { + return 'Failed to get location: $error'; + } + + @override + String currentCoordinates(double latitude, double longitude) { + return 'Current coordinates: Lat $latitude, Lng $longitude'; + } + + @override + String get errorLoadingDrones => 'Error loading drones'; + + @override + String get errorLoadingBatteries => 'Error loading batteries'; + + @override + String get noDronesYet => 'No drones added yet. Tap the \'+\' card to add one!'; + + @override + String get noBatteriesYet => 'No batteries added yet. Tap the \'+\' card to add one!'; + + @override + String get flightSavedSuccessfully => 'Flight saved successfully!'; + + @override + String failedToSaveFlight(String error) { + return 'Failed to save flight: $error'; + } + + @override + String get errorLoadingFlights => 'Error loading flights'; + + @override + String get noFlightsYet => 'No flights recorded yet.'; + + @override + String get droneName => 'Drone Name'; + + @override + String get pleaseEnterDroneName => 'Please enter a drone name'; + + @override + String get imageUuidOptional => 'Image UUID (Optional)'; + + @override + String get saveDrone => 'Save Drone'; + + @override + String get droneAddedSuccessfully => 'Drone added successfully!'; + + @override + String get failedToAddDrone => 'Failed to add drone'; + + @override + String get batteryName => 'Battery Name'; + + @override + String get batteryType => 'Battery Type'; + + @override + String get batteryVoltage => 'Voltage (V)'; + + @override + String get pleaseEnterBatteryName => 'Please enter a battery name'; + + @override + String get pleaseEnterBatteryType => 'Please enter a battery type'; + + @override + String get pleaseEnterBatteryVoltage => 'Please enter battery voltage'; + + @override + String get pleaseEnterValidNumber => 'Please enter a valid number'; + + @override + String get saveBattery => 'Save Battery'; + + @override + String get batteryAddedSuccessfully => 'Battery added successfully!'; + + @override + String get failedToAddBattery => 'Failed to add battery'; + + @override + String get droneImage => 'Drone Image'; + + @override + String get batteryImage => 'Battery Image'; + + @override + String get takePhoto => 'Take Photo'; + + @override + String get chooseFromGallery => 'Choose from Gallery'; + + @override + String get imageSelected => 'Image selected!'; + + @override + String get imageSelectionCancelled => 'Image selection cancelled.'; + + @override + String get removeImage => 'Remove Image'; + + @override + String get imageDeletedSuccessfully => 'Image deleted successfully!'; + + @override + String get droneDetails => 'Drone Details'; + + @override + String get editDrone => 'Edit Drone'; + + @override + String get saveChanges => 'Save Changes'; + + @override + String get droneUpdatedSuccessfully => 'Drone updated successfully!'; + + @override + String get failedToUpdateDrone => 'Failed to update drone'; + + @override + String get deleteDroneConfirmationTitle => 'Delete Drone?'; + + @override + String deleteDroneConfirmationMessage(String droneName) { + return 'Are you sure you want to delete $droneName? This action cannot be undone.'; + } + + @override + String get droneDeletedSuccessfully => 'Drone deleted successfully!'; + + @override + String get failedToDeleteDrone => 'Failed to delete drone'; + + @override + String get batteryDetails => 'Battery Details'; + + @override + String get editBattery => 'Edit Battery'; + + @override + String get batteryUpdatedSuccessfully => 'Battery updated successfully!'; + + @override + String get failedToUpdateBattery => 'Failed to update battery'; + + @override + String get deleteBatteryConfirmationTitle => 'Delete Battery?'; + + @override + String deleteBatteryConfirmationMessage(String batteryName) { + return 'Are you sure you want to delete $batteryName? This action cannot be undone.'; + } + + @override + String get batteryDeletedSuccessfully => 'Battery deleted successfully!'; + + @override + String get failedToDeleteBattery => 'Failed to delete battery'; + + @override + String get cancel => 'Cancel'; + + @override + String get delete => 'Delete'; + + @override + String get flightDetails => 'Flight Details'; + + @override + String get startTime => 'Start Time'; + + @override + String get endTime => 'End Time'; + + @override + String get duration => 'Duration'; + + @override + String get flightLocation => 'Flight Location'; + + @override + String get noLocationData => 'No location data for this flight.'; + + @override + String get unknown => 'Unknown'; + + @override + String get errorLoadingData => 'Error loading data'; + + @override + String get drone => 'Drone'; + + @override + String get battery => 'Battery'; + + @override + String get themeSetting => 'Theme'; + + @override + String get themeLight => 'Light Theme'; + + @override + String get themeDark => 'Dark Theme'; + + @override + String get themeSystem => 'System Default'; + + @override + String themeChangedTo(String themeName) { + return 'Theme changed to $themeName'; + } + + @override + String get deleteFlightConfirmationTitle => 'Delete Flight?'; + + @override + String deleteFlightConfirmationMessage(String flightName) { + return 'Are you sure you want to delete $flightName? This action cannot be undone.'; + } + + @override + String get flightDeletedSuccessfully => 'Flight deleted successfully!'; + + @override + String get failedToDeleteFlight => 'Failed to delete flight'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..a1b042f --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,345 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'Rtime'; + + @override + String get yourDrones => 'Vos Drones'; + + @override + String get addDrone => 'Ajouter un drone'; + + @override + String get yourBatteries => 'Vos Batteries'; + + @override + String get addBattery => 'Ajouter une batterie'; + + @override + String get latestFlights => 'Derniers Vols'; + + @override + String get newFlight => 'Nouveau Vol'; + + @override + String detailsOfDrone(String droneName) { + return 'Détails du drone : $droneName'; + } + + @override + String detailsOfBattery(String batteryName) { + return 'Détails de la batterie : $batteryName'; + } + + @override + String detailsOfFlight(String flightName) { + return 'Détails du vol : $flightName'; + } + + @override + String get settingsTitle => 'Paramètres'; + + @override + String get languageSetting => 'Langue'; + + @override + String get english => 'Anglais'; + + @override + String get french => 'Français'; + + @override + String languageChangedTo(String languageName) { + return 'Langue changée en $languageName'; + } + + @override + String get chooseDrone => 'Choisissez votre Drone'; + + @override + String get selectDroneHint => 'Sélectionnez un drone'; + + @override + String get chooseBattery => 'Choisissez votre Batterie'; + + @override + String get selectBatteryHint => 'Sélectionnez une batterie'; + + @override + String get startFlight => 'Lancer le vol'; + + @override + String get stopFlight => 'Arrêter le vol'; + + @override + String get selectDroneBattery => 'Veuillez sélectionner un drone et une batterie.'; + + @override + String get flightStarted => 'Vol démarré !'; + + @override + String flightStopped(String flightDuration) { + return 'Vol arrêté. Durée : $flightDuration'; + } + + @override + String get recordFlightLocation => 'Enregistrer la localisation du vol (GPS)'; + + @override + String get locationServicesDisabled => 'Les services de localisation sont désactivés.'; + + @override + String get locationPermissionsDenied => 'Les permissions de localisation sont refusées.'; + + @override + String get locationPermissionsDeniedForever => 'Les permissions de localisation sont refusées de manière permanente, nous ne pouvons pas les demander.'; + + @override + String locationObtained(double latitude, double longitude) { + return 'Localisation obtenue : Lat $latitude, Lon $longitude'; + } + + @override + String get locationRequiredForFlight => 'La localisation GPS est requise mais n\'a pu être obtenue. Veuillez vérifier les permissions et réessayer.'; + + @override + String get obtainingLocation => 'Obtention de la localisation...'; + + @override + String failedToGetLocation(String error) { + return 'Échec de l\'obtention de la localisation : $error'; + } + + @override + String currentCoordinates(double latitude, double longitude) { + return 'Coordonnées actuelles : Lat $latitude, Lon $longitude'; + } + + @override + String get errorLoadingDrones => 'Erreur lors du chargement des drones'; + + @override + String get errorLoadingBatteries => 'Erreur lors du chargement des batteries'; + + @override + String get noDronesYet => 'Aucun drone ajouté pour le moment. Appuyez sur la carte \'+\' pour en ajouter un !'; + + @override + String get noBatteriesYet => 'Aucune batterie ajoutée pour le moment. Appuyez sur la carte \'+\' pour en ajouter une !'; + + @override + String get flightSavedSuccessfully => 'Vol enregistré avec succès !'; + + @override + String failedToSaveFlight(String error) { + return 'Échec de l\'enregistrement du vol : $error'; + } + + @override + String get errorLoadingFlights => 'Erreur lors du chargement des vols'; + + @override + String get noFlightsYet => 'Aucun vol enregistré pour le moment.'; + + @override + String get droneName => 'Nom du drone'; + + @override + String get pleaseEnterDroneName => 'Veuillez entrer un nom de drone'; + + @override + String get imageUuidOptional => 'UUID de l\'image (Facultatif)'; + + @override + String get saveDrone => 'Enregistrer le drone'; + + @override + String get droneAddedSuccessfully => 'Drone ajouté avec succès !'; + + @override + String get failedToAddDrone => 'Échec de l\'ajout du drone'; + + @override + String get batteryName => 'Nom de la batterie'; + + @override + String get batteryType => 'Type de batterie'; + + @override + String get batteryVoltage => 'Tension (V)'; + + @override + String get pleaseEnterBatteryName => 'Veuillez entrer un nom de batterie'; + + @override + String get pleaseEnterBatteryType => 'Veuillez entrer un type de batterie'; + + @override + String get pleaseEnterBatteryVoltage => 'Veuillez entrer la tension de la batterie'; + + @override + String get pleaseEnterValidNumber => 'Veuillez entrer un nombre valide'; + + @override + String get saveBattery => 'Enregistrer la batterie'; + + @override + String get batteryAddedSuccessfully => 'Batterie ajoutée avec succès !'; + + @override + String get failedToAddBattery => 'Échec de l\'ajout de la batterie'; + + @override + String get droneImage => 'Image du drone'; + + @override + String get batteryImage => 'Image de la batterie'; + + @override + String get takePhoto => 'Prendre une photo'; + + @override + String get chooseFromGallery => 'Choisir de la galerie'; + + @override + String get imageSelected => 'Image sélectionnée !'; + + @override + String get imageSelectionCancelled => 'Sélection d\'image annulée.'; + + @override + String get removeImage => 'Supprimer l\'image'; + + @override + String get imageDeletedSuccessfully => 'Image supprimée avec succès !'; + + @override + String get droneDetails => 'Détails du drone'; + + @override + String get editDrone => 'Modifier le drone'; + + @override + String get saveChanges => 'Enregistrer les modifications'; + + @override + String get droneUpdatedSuccessfully => 'Drone mis à jour avec succès !'; + + @override + String get failedToUpdateDrone => 'Échec de la mise à jour du drone'; + + @override + String get deleteDroneConfirmationTitle => 'Supprimer le drone ?'; + + @override + String deleteDroneConfirmationMessage(String droneName) { + return 'Êtes-vous sûr de vouloir supprimer $droneName ? Cette action est irréversible.'; + } + + @override + String get droneDeletedSuccessfully => 'Drone supprimé avec succès !'; + + @override + String get failedToDeleteDrone => 'Échec de la suppression du drone'; + + @override + String get batteryDetails => 'Détails de la batterie'; + + @override + String get editBattery => 'Modifier la batterie'; + + @override + String get batteryUpdatedSuccessfully => 'Batterie mise à jour avec succès !'; + + @override + String get failedToUpdateBattery => 'Échec de la mise à jour de la batterie'; + + @override + String get deleteBatteryConfirmationTitle => 'Supprimer la batterie ?'; + + @override + String deleteBatteryConfirmationMessage(String batteryName) { + return 'Êtes-vous sûr de vouloir supprimer $batteryName ? Cette action est irréversible.'; + } + + @override + String get batteryDeletedSuccessfully => 'Batterie supprimée avec succès !'; + + @override + String get failedToDeleteBattery => 'Échec de la suppression de la batterie'; + + @override + String get cancel => 'Annuler'; + + @override + String get delete => 'Supprimer'; + + @override + String get flightDetails => 'Détails du vol'; + + @override + String get startTime => 'Heure de début'; + + @override + String get endTime => 'Heure de fin'; + + @override + String get duration => 'Durée'; + + @override + String get flightLocation => 'Localisation du vol'; + + @override + String get noLocationData => 'Pas de données de localisation pour ce vol.'; + + @override + String get unknown => 'Inconnu'; + + @override + String get errorLoadingData => 'Erreur de chargement des données'; + + @override + String get drone => 'Drone'; + + @override + String get battery => 'Batterie'; + + @override + String get themeSetting => 'Thème'; + + @override + String get themeLight => 'Thème Clair'; + + @override + String get themeDark => 'Thème Sombre'; + + @override + String get themeSystem => 'Par défaut du système'; + + @override + String themeChangedTo(String themeName) { + return 'Thème changé en $themeName'; + } + + @override + String get deleteFlightConfirmationTitle => 'Supprimer le vol ?'; + + @override + String deleteFlightConfirmationMessage(String flightName) { + return 'Êtes-vous sûr de vouloir supprimer $flightName ? Cette action est irréversible.'; + } + + @override + String get flightDeletedSuccessfully => 'Vol supprimé avec succès !'; + + @override + String get failedToDeleteFlight => 'Échec de la suppression du vol'; +} diff --git a/lib/main.dart b/lib/main.dart index f398d80..2b24d59 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,38 +1,229 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:logging/logging.dart'; -import 'package:rtime/db/db_helper.dart'; -import 'package:rtime/images_manager.dart'; -import 'package:rtime/models/drone.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:rtime/l10n/app_localizations.dart'; import 'package:rtime/pages/home_page.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; +import 'package:rtime/providers/local_provider.dart'; +import 'package:rtime/providers/theme_provider.dart'; void main() { - if (Platform.isWindows || Platform.isLinux) { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - } - - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen( - (record) => - print('${record.level.name}: ${record.time}: ${record.message}'), + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => LocaleProvider(), + ), + ChangeNotifierProvider( + create: (context) => ThemeProvider(), + ), + ], + child: const MyApp(), + ), ); - runApp(const RTimeApp()); - - //DbHelper.instance.closeDb(); } -class RTimeApp extends StatelessWidget { - const RTimeApp({super.key}); +class MyApp extends StatefulWidget { + const MyApp({super.key}); - // This widget is the root of your application. + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { @override Widget build(BuildContext context) { - return Text("RtimeAPP"); - } + return Consumer2( + builder: (context, localeProvider, themeProvider, child) { + return MaterialApp( + title: 'Rtime', + locale: localeProvider.locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + Locale('fr', ''), + ], + + theme: ThemeData( + primaryColor: Colors.white, + colorScheme: ColorScheme.light( + primary: Colors.blue.shade600, + secondary: Colors.teal.shade400, + surface: Colors.white, + background: Colors.white, + error: Colors.red.shade700, + onPrimary: Colors.white, + onSecondary: Colors.white, + onSurface: Colors.black87, + onBackground: Colors.black87, + ), + scaffoldBackgroundColor: Colors.white, + + appBarTheme: AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 0.5, + iconTheme: const IconThemeData(color: Colors.black87), + titleTextStyle: const TextStyle( + color: Colors.black87, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + extendedSizeConstraints: const BoxConstraints.tightFor( + height: 70.0, + width: 250.0, + ), + ), + cardTheme: CardThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + color: Colors.white, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + listTileTheme: ListTileThemeData( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + tileColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + selectedTileColor: Colors.blue.shade50, + ), + textTheme: const TextTheme( + headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87), + titleSmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black54), + bodyLarge: TextStyle(color: Colors.black87), + bodyMedium: TextStyle(color: Colors.black54), + bodySmall: TextStyle(fontSize: 12, color: Colors.black45), + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + buttonTheme: ButtonThemeData( + buttonColor: Colors.blue.shade600, + textTheme: ButtonTextTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.blue.shade600, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + labelStyle: const TextStyle(color: Colors.black54), + hintStyle: TextStyle(color: Colors.grey.shade500), + ), + ), + + + darkTheme: ThemeData.dark().copyWith( + primaryColor: Colors.blueGrey[900], + colorScheme: ColorScheme.dark( + primary: Colors.lightBlueAccent, + secondary: Colors.tealAccent, + surface: Colors.blueGrey.shade800, + background: Colors.blueGrey.shade900, + onPrimary: Colors.black, + onSecondary: Colors.black, + onSurface: Colors.white, + onBackground: Colors.white, + ), + + + + appBarTheme: AppBarTheme( + backgroundColor: Colors.blueGrey[900], + foregroundColor: Colors.white, + elevation: 4, + iconTheme: const IconThemeData(color: Colors.white), + titleTextStyle: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: Colors.lightBlueAccent, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + extendedSizeConstraints: const BoxConstraints.tightFor( + height: 70.0, + width: 250.0, + ), + ), + cardTheme: CardThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 8, + color: Colors.blueGrey[800], + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + listTileTheme: ListTileThemeData( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + tileColor: Colors.blueGrey[800], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + selectedTileColor: Colors.blueGrey[700], + ), + textTheme: const TextTheme( + headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), + titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white), + titleSmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + bodyLarge: TextStyle(color: Colors.white70), + bodyMedium: TextStyle(color: Colors.white60), + bodySmall: TextStyle(fontSize: 12, color: Colors.white70), + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + buttonTheme: ButtonThemeData( + buttonColor: Colors.lightBlueAccent, + textTheme: ButtonTextTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.blueGrey.shade600), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.lightBlueAccent, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.blueGrey.shade700), + ), + labelStyle: const TextStyle(color: Colors.white70), + hintStyle: TextStyle(color: Colors.blueGrey.shade500), + ), + ), + themeMode: themeProvider.themeMode, + home: const HomePage(), + ); + }, + ); + } } diff --git a/lib/models/flight.dart b/lib/models/flight.dart index 2f40094..c4e0584 100644 --- a/lib/models/flight.dart +++ b/lib/models/flight.dart @@ -31,7 +31,7 @@ class Flight { startTimestamp: map["start_timestamp"], endTimestamp: map["end_timestamp"], droneId: map["drone_id"], - batteryId: map["batteryId"], + batteryId: map["battery_id"], locationLat: map["location_lat"], locationLong: map["location_long"], ); diff --git a/lib/pages/battery_detail_page.dart b/lib/pages/battery_detail_page.dart new file mode 100644 index 0000000..cad1354 --- /dev/null +++ b/lib/pages/battery_detail_page.dart @@ -0,0 +1,425 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:rtime/images_manager.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:rtime/models/flight.dart'; +import 'package:rtime/pages/flight_detail_page.dart'; + +class BatteryDetailPage extends StatefulWidget { + final Battery battery; + + const BatteryDetailPage({super.key, required this.battery}); + + @override + State createState() => _BatteryDetailPageState(); +} + +class _BatteryDetailPageState extends State { + final _formKey = GlobalKey(); + late TextEditingController _nameController; + late TextEditingController _typeController; + late TextEditingController _voltageController; + String? _currentImageUuid; + Image? _displayImage; + bool _isEditing = false; + List _associatedFlights = []; + bool _isLoadingFlights = true; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.battery.name); + _typeController = TextEditingController(text: widget.battery.type); + _voltageController = TextEditingController(text: widget.battery.voltage.toString()); + _currentImageUuid = widget.battery.imageUuid; + _loadImage(); + _loadAssociatedFlights(); + } + + @override + void dispose() { + _nameController.dispose(); + _typeController.dispose(); + _voltageController.dispose(); + super.dispose(); + } + + Future _loadImage() async { + if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) { + final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!); + setState(() { + _displayImage = loadedImage; + }); + } else { + setState(() { + _displayImage = null; + }); + } + } + + + Future _loadAssociatedFlights() async { + setState(() { + _isLoadingFlights = true; + }); + try { + final allFlights = await DbHelper.instance.getBatteryFlights(widget.battery.id!); + + _associatedFlights = allFlights + .where((flight) => flight.batteryId == widget.battery.id) + .toList(); + + _associatedFlights.sort((a, b) => b.startTimestamp.compareTo(a.startTimestamp)); + + setState(() { + _isLoadingFlights = false; + }); + } catch (e) { + if (mounted) { + + + + + setState(() { + _isLoadingFlights = false; + }); + } + } + } + + Future _pickImage(ImageSource source) async { + final uuid = await ImagesManager.instance.createImage(source); + + if (uuid != null) { + + if (_currentImageUuid != null && _currentImageUuid != uuid) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + } + setState(() { + _currentImageUuid = uuid; + }); + await _loadImage(); + } + } + + Future _deleteImage() async { + if (_currentImageUuid != null) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + setState(() { + _currentImageUuid = null; + _displayImage = null; + }); + } + } + + Future _saveBattery() async { + if (_formKey.currentState!.validate()) { + final l10n = AppLocalizations.of(context)!; + final updatedBattery = Battery( + id: widget.battery.id, + name: _nameController.text.trim(), + type: _typeController.text.trim(), + voltage: double.parse(_voltageController.text.trim()), + imageUuid: _currentImageUuid, + ); + + try { + await DbHelper.instance.updateBattery(updatedBattery); + if (mounted) { + + + + Navigator.of(context).pop(true); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + Future _deleteBattery() async { + final l10n = AppLocalizations.of(context)!; + final bool? confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.deleteBatteryConfirmationTitle), + content: Text(l10n.deleteBatteryConfirmationMessage(widget.battery.name)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.delete), + ), + ], + ), + ); + + if (confirm == true) { + try { + + if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + } + await DbHelper.instance.deleteBattery(widget.battery.id!); + if (mounted) { + + + + Navigator.of(context).pop(true); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(_isEditing ? l10n.editBattery : l10n.batteryDetails), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + IconButton( + icon: Icon(_isEditing ? Icons.save : Icons.edit), + onPressed: () { + if (_isEditing) { + _saveBattery(); + } + setState(() { + _isEditing = !_isEditing; + }); + }, + ), + if (!_isEditing) + IconButton( + icon: const Icon(Icons.delete_forever, color: Colors.redAccent), + onPressed: _deleteBattery, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + Center( + child: Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: Colors.blueGrey[800], + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.tealAccent, width: 2), + image: _displayImage != null + ? DecorationImage( + image: _displayImage!.image, + fit: BoxFit.cover, + ) + : null, + ), + child: _displayImage == null + ? Icon( + Icons.camera_alt, + size: 80, + color: Colors.blueGrey[400], + ) + : null, + ), + ), + if (_isEditing) ...[ + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: Text(l10n.takePhoto), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + const SizedBox(width: 10), + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.gallery), + icon: const Icon(Icons.photo_library), + label: Text(l10n.chooseFromGallery), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + ], + ), + if (_currentImageUuid != null) + TextButton.icon( + onPressed: _deleteImage, + icon: const Icon(Icons.clear, color: Colors.redAccent), + label: Text(l10n.removeImage, style: const TextStyle(color: Colors.redAccent)), + ), + ], + const SizedBox(height: 30), + TextFormField( + controller: _nameController, + readOnly: !_isEditing, + decoration: InputDecoration( + labelText: l10n.batteryName, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.tealAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryName; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _typeController, + readOnly: !_isEditing, + decoration: InputDecoration( + labelText: l10n.batteryType, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.tealAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryType; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _voltageController, + keyboardType: TextInputType.number, + readOnly: !_isEditing, + decoration: InputDecoration( + labelText: l10n.batteryVoltage, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.tealAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryVoltage; + } + if (double.tryParse(value) == null) { + return l10n.pleaseEnterValidNumber; + } + return null; + }, + ), + const SizedBox(height: 30), + if (_isEditing) + Center( + child: ElevatedButton( + onPressed: _saveBattery, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + l10n.saveChanges, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ), + + const SizedBox(height: 30), + Text( + l10n.latestFlights, + style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white), + ), + const SizedBox(height: 15), + _isLoadingFlights + ? const Center(child: CircularProgressIndicator()) + : _associatedFlights.isEmpty + ? Center( + child: Text( + l10n.noFlightsYet, + style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70), + ), + ) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _associatedFlights.length, + itemBuilder: (context, index) { + final flight = _associatedFlights[index]; + final DateTime flightDate = + DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000); + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + leading: const Icon(Icons.flight_takeoff, + color: Colors.orangeAccent, size: 32), + title: Text( + '${flight.name} - ${flightDate.toLocal().toString().split(' ')[0]}', + style: Theme.of(context).textTheme.titleMedium, + ), + trailing: const Icon(Icons.arrow_forward_ios, + size: 20, color: Colors.white54), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FlightDetailPage(flight: flight), + ), + ).then((_) => _loadAssociatedFlights()); + }, + ), + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/drone_detail_page.dart b/lib/pages/drone_detail_page.dart new file mode 100644 index 0000000..9ab016d --- /dev/null +++ b/lib/pages/drone_detail_page.dart @@ -0,0 +1,367 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:rtime/images_manager.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:rtime/models/flight.dart'; +import 'package:rtime/pages/flight_detail_page.dart'; + +class DroneDetailPage extends StatefulWidget { + final Drone drone; + + const DroneDetailPage({super.key, required this.drone}); + + @override + State createState() => _DroneDetailPageState(); +} + +class _DroneDetailPageState extends State { + final _formKey = GlobalKey(); + late TextEditingController _nameController; + String? _currentImageUuid; + Image? _displayImage; + bool _isEditing = false; + List _associatedFlights = []; + bool _isLoadingFlights = true; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.drone.name); + _currentImageUuid = widget.drone.imageUuid; + _loadImage(); + _loadAssociatedFlights(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _loadImage() async { + if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) { + final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!); + setState(() { + _displayImage = loadedImage; + }); + } else { + setState(() { + _displayImage = null; + }); + } + } + + + Future _loadAssociatedFlights() async { + setState(() { + _isLoadingFlights = true; + }); + try { + final allFlights = await DbHelper.instance.getDroneFlights(widget.drone.id!); + + _associatedFlights = allFlights + .where((flight) => flight.droneId == widget.drone.id) + .toList(); + + _associatedFlights.sort((a, b) => b.startTimestamp.compareTo(a.startTimestamp)); + + setState(() { + _isLoadingFlights = false; + }); + } catch (e) { + if (mounted) { + + + + + setState(() { + _isLoadingFlights = false; + }); + } + } + } + + Future _pickImage(ImageSource source) async { + final uuid = await ImagesManager.instance.createImage(source); + + if (uuid != null) { + if (_currentImageUuid != null && _currentImageUuid != uuid) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + } + setState(() { + _currentImageUuid = uuid; + }); + await _loadImage(); + } + } + + Future _deleteImage() async { + if (_currentImageUuid != null) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + setState(() { + _currentImageUuid = null; + _displayImage = null; + }); + } + } + + Future _saveDrone() async { + if (_formKey.currentState!.validate()) { + final l10n = AppLocalizations.of(context)!; + final updatedDrone = Drone( + id: widget.drone.id, + name: _nameController.text.trim(), + imageUuid: _currentImageUuid, + ); + + try { + await DbHelper.instance.updateDrone(updatedDrone); + if (mounted) { + + + + Navigator.of(context).pop(true); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + Future _deleteDrone() async { + final l10n = AppLocalizations.of(context)!; + final bool? confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.deleteDroneConfirmationTitle), + content: Text(l10n.deleteDroneConfirmationMessage(widget.drone.name)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.delete), + ), + ], + ), + ); + + if (confirm == true) { + try { + if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) { + await ImagesManager.instance.deleteImage(_currentImageUuid!); + } + await DbHelper.instance.deleteDrone(widget.drone.id!); + if (mounted) { + + + + Navigator.of(context).pop(true); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(_isEditing ? l10n.editDrone : l10n.droneDetails), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + IconButton( + icon: Icon(_isEditing ? Icons.save : Icons.edit), + onPressed: () { + if (_isEditing) { + _saveDrone(); + } + setState(() { + _isEditing = !_isEditing; + }); + }, + ), + if (!_isEditing) + IconButton( + icon: const Icon(Icons.delete_forever, color: Colors.redAccent), + onPressed: _deleteDrone, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + Center( + child: Container( + width: 180, + height: 180, + decoration: BoxDecoration( + color: Colors.blueGrey[800], + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.lightBlueAccent, width: 2), + image: _displayImage != null + ? DecorationImage( + image: _displayImage!.image, + fit: BoxFit.cover, + ) + : null, + ), + child: _displayImage == null + ? Icon( + Icons.camera_alt, + size: 80, + color: Colors.blueGrey[400], + ) + : null, + ), + ), + if (_isEditing) ...[ + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: Text(l10n.takePhoto), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + const SizedBox(width: 10), + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.gallery), + icon: const Icon(Icons.photo_library), + label: Text(l10n.chooseFromGallery), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + ], + ), + if (_currentImageUuid != null) + TextButton.icon( + onPressed: _deleteImage, + icon: const Icon(Icons.clear, color: Colors.redAccent), + label: Text(l10n.removeImage, style: const TextStyle(color: Colors.redAccent)), + ), + ], + const SizedBox(height: 30), + TextFormField( + controller: _nameController, + readOnly: !_isEditing, + decoration: InputDecoration( + labelText: l10n.droneName, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterDroneName; + } + return null; + }, + ), + const SizedBox(height: 30), + if (_isEditing) + Center( + child: ElevatedButton( + onPressed: _saveDrone, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + l10n.saveChanges, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ), + + const SizedBox(height: 30), + Text( + l10n.latestFlights, + style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white), + ), + const SizedBox(height: 15), + _isLoadingFlights + ? const Center(child: CircularProgressIndicator()) + : _associatedFlights.isEmpty + ? Center( + child: Text( + l10n.noFlightsYet, + style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70), + ), + ) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _associatedFlights.length, + itemBuilder: (context, index) { + final flight = _associatedFlights[index]; + final DateTime flightDate = + DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000); + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + leading: const Icon(Icons.flight_takeoff, + color: Colors.orangeAccent, size: 32), + title: Text( + '${flight.name} - ${flightDate.toLocal().toString().split(' ')[0]}', + style: Theme.of(context).textTheme.titleMedium, + ), + trailing: const Icon(Icons.arrow_forward_ios, + size: 20, color: Colors.white54), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FlightDetailPage(flight: flight), + ), + ).then((_) => _loadAssociatedFlights()); + }, + ), + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/flight_detail_page.dart b/lib/pages/flight_detail_page.dart new file mode 100644 index 0000000..31a87b5 --- /dev/null +++ b/lib/pages/flight_detail_page.dart @@ -0,0 +1,373 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/flight.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:rtime/images_manager.dart'; + +class FlightDetailPage extends StatefulWidget { + final Flight flight; + + const FlightDetailPage({super.key, required this.flight}); + + @override + State createState() => _FlightDetailPageState(); +} + +class _FlightDetailPageState extends State { + Drone? _associatedDrone; + Battery? _associatedBattery; + Image? _droneDisplayImage; + Image? _batteryDisplayImage; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadAssociatedData(); + } + + Future _loadImage(String? uuid, Function(Image?) setImage) async { + if (uuid != null && uuid.isNotEmpty) { + final loadedImage = await ImagesManager.instance.loadImage(uuid); + if (mounted) { + setImage(loadedImage); + } + } else { + if (mounted) { + setImage(null); + } + } + } + + Future _loadAssociatedData() async { + try { + final droneList = await DbHelper.instance.getDrones(); + final batteryList = await DbHelper.instance.getBatteries(); + + final drone = droneList.firstWhere( + (d) => d.id == widget.flight.droneId, + orElse: () => Drone(name: 'Unknown Drone')); + + final battery = batteryList.firstWhere( + (b) => b.id == widget.flight.batteryId, + orElse: () => Battery( + name: 'Unknown Battery', type: '', voltage: 0.0)); + + + await _loadImage(drone.imageUuid, (image) { + setState(() { + _droneDisplayImage = image; + }); + }); + await _loadImage(battery.imageUuid, (image) { + setState(() { + _batteryDisplayImage = image; + }); + }); + + setState(() { + _associatedDrone = drone; + _associatedBattery = battery; + _isLoading = false; + }); + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + String _formatDuration(int startTimestamp, int endTimestamp) { + final duration = Duration(seconds: endTimestamp - startTimestamp); + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String hours = twoDigits(duration.inHours); + String minutes = twoDigits(duration.inMinutes.remainder(60)); + String seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$hours:$minutes:$seconds'; + } + + Future _deleteFlight() async { + final l10n = AppLocalizations.of(context)!; + final bool? confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.deleteFlightConfirmationTitle), + content: Text(l10n.deleteFlightConfirmationMessage(widget.flight.name)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.delete), + ), + ], + ), + ); + + if (confirm == true) { + try { + await DbHelper.instance.deleteFlight(widget.flight.id!); + if (mounted) { + Navigator.of(context).pop(true); + } + } catch (e) { + if (mounted) { + + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final flight = widget.flight; + + final DateTime startDate = + DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000); + final DateTime endDate = + DateTime.fromMillisecondsSinceEpoch(flight.endTimestamp * 1000); + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(l10n.flightDetails), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.delete_forever, color: Colors.redAccent), + onPressed: _deleteFlight, + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 4, + margin: const EdgeInsets.only(bottom: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + flight.name, + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Divider(color: Colors.grey[300]), + + _buildImageAndDetailRow( + context, + l10n.drone, + _associatedDrone?.name ?? l10n.unknown, + _droneDisplayImage, + Icons.flight_takeoff, + ), + + _buildImageAndDetailRow( + context, + l10n.battery, + _associatedBattery?.name ?? l10n.unknown, + _batteryDisplayImage, + Icons.battery_std, + ), + _buildDetailRow( + l10n.startTime, + '${startDate.toLocal().toString().split(' ')[0]} ${startDate.toLocal().toString().split(' ')[1].substring(0, 5)}'), + _buildDetailRow( + l10n.endTime, + '${endDate.toLocal().toString().split(' ')[0]} ${endDate.toLocal().toString().split(' ')[1].substring(0, 5)}'), + _buildDetailRow( + l10n.duration, + _formatDuration( + flight.startTimestamp, flight.endTimestamp)), + ], + ), + ), + ), + + + if (flight.locationLat != null && flight.locationLong != null) ...[ + Text( + l10n.flightLocation, + style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white), + ), + const SizedBox(height: 15), + Container( + height: 300, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(15), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: FlutterMap( + options: MapOptions( + initialCenter: LatLng( + flight.locationLat!, flight.locationLong!), + initialZoom: 15.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + ), + children: [ + TileLayer( + urlTemplate: + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + userAgentPackageName: 'com.example.rtime', + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng( + flight.locationLat!, flight.locationLong!), + width: 80, + height: 80, + child: const Icon( + Icons.location_on, + color: Colors.red, + size: 40.0, + ), + ), + ], + ), + ], + ), + ), + ), + ] else ...[ + const SizedBox(height: 20), + Center( + child: Text( + l10n.noLocationData, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(color: Colors.white70), + ), + ), + ], + const SizedBox(height: 20), + ], + ), + ), + ); + } + + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + '$label:', + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + flex: 3, + child: Text( + value, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ); + } + + + Widget _buildImageAndDetailRow( + BuildContext context, String label, String value, Image? displayImage, IconData defaultIcon) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + + Container( + width: 40, + height: 40, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: Colors.blueGrey[800], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blueGrey[600]!, width: 1), + image: displayImage != null + ? DecorationImage( + image: displayImage.image, + fit: BoxFit.cover, + ) + : null, + ), + child: displayImage == null + ? Icon( + defaultIcon, + size: 24, + color: Colors.blueGrey[400], + ) + : null, + ), + + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + '$label:', + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + flex: 3, + child: Text( + value, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..2469dac --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,389 @@ + +import 'package:flutter/material.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/models/flight.dart'; +import 'package:rtime/widgets/drone_cart.dart'; +import 'package:rtime/widgets/battery_card.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/pages/settings_page.dart'; +import 'package:rtime/pages/new_flight_page.dart'; +import 'package:rtime/pages/new_drone_page.dart'; +import 'package:rtime/pages/new_battery_page.dart'; +import 'package:rtime/pages/drone_detail_page.dart'; +import 'package:rtime/pages/battery_detail_page.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:rtime/pages/flight_detail_page.dart'; + + +import 'package:rtime/widgets/page_transition_animations.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + late Future> _dronesFuture; + late Future> _batteriesFuture; + late Future> _flightsFuture; + + @override + void initState() { + super.initState(); + _loadData(); + } + + void _loadData() { + setState(() { + _dronesFuture = DbHelper.instance.getDrones(); + _batteriesFuture = DbHelper.instance.getBatteries(); + _flightsFuture = DbHelper.instance.getFlights(); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + body: RefreshIndicator( + onRefresh: () async { + _loadData(); + await Future.wait([_dronesFuture, _batteriesFuture, _flightsFuture]); + }, + child: SingleChildScrollView( + padding: const EdgeInsets.only( + top: 60.0, left: 16.0, right: 16.0, bottom: 90.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10n.appTitle, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Colors.lightBlueAccent, + fontFamily: 'Montserrat', + ), + ), + IconButton( + icon: + const Icon(Icons.settings, size: 30, color: Colors.white70), + onPressed: () { + + Navigator.of(context).push( + SlideRightPageRoute( + page: const SettingsPage(), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 30), + + + Text( + l10n.yourDrones, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 15), + SizedBox( + height: 170, + child: FutureBuilder>( + future: _dronesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text( + '${l10n.errorLoadingDrones}: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Card( + margin: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: () { + + Navigator.of(context).push( + SlideUpPageRoute( + page: const NewDronePage(), + ), + ).then((_) => _loadData()); + }, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: 140, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, + size: 50, color: Colors.blueGrey[400]), + const SizedBox(height: 10), + Text(l10n.addDrone, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall), + ], + ), + ), + ), + ); + } else { + final drones = snapshot.data!; + return ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: drones.length + 1, + itemBuilder: (context, index) { + if (index == drones.length) { + return Card( + margin: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: () { + + Navigator.of(context).push( + SlideUpPageRoute( + page: const NewDronePage(), + ), + ).then((_) => _loadData()); + }, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: 140, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, + size: 50, color: Colors.blueGrey[400]), + const SizedBox(height: 10), + Text(l10n.addDrone, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall), + ], + ), + ), + ), + ); + } + return DroneCard( + drone: drones[index], + onTap: () { + + Navigator.of(context).push( + SlideRightPageRoute( + page: DroneDetailPage(drone: drones[index]), + ), + ).then((result) { + + if (result == true) { + _loadData(); + } + }); + }, + ); + }, + ); + } + }, + ), + ), + const Divider( + height: 50, thickness: 2, indent: 0, endIndent: 0, color: Colors.white10), + + + Text( + l10n.yourBatteries, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 15), + SizedBox( + height: 170, + child: FutureBuilder>( + future: _batteriesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text( + '${l10n.errorLoadingBatteries}: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Card( + margin: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: () { + + Navigator.of(context).push( + SlideUpPageRoute( + page: const NewBatteryPage(), + ), + ).then((_) => _loadData()); + }, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: 140, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, + size: 50, color: Colors.blueGrey[400]), + const SizedBox(height: 10), + Text(l10n.addBattery, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall), + ], + ), + ), + ), + ); + } else { + final batteries = snapshot.data!; + return ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: batteries.length + 1, + itemBuilder: (context, index) { + if (index == batteries.length) { + return Card( + margin: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: () { + + Navigator.of(context).push( + SlideUpPageRoute( + page: const NewBatteryPage(), + ), + ).then((_) => _loadData()); + }, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: 140, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline, + size: 50, color: Colors.blueGrey[400]), + const SizedBox(height: 10), + Text(l10n.addBattery, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall), + ], + ), + ), + ), + ); + } + return BatteryCard( + battery: batteries[index], + onTap: () { + + Navigator.of(context).push( + SlideRightPageRoute( + page: BatteryDetailPage(battery: batteries[index]), + ), + ).then((result) { + + if (result == true) { + _loadData(); + } + }); + }, + ); + }, + ); + } + }, + ), + ), + const Divider( + height: 50, thickness: 2, indent: 0, endIndent: 0, color: Colors.white10), + + + Text( + l10n.latestFlights, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 15), + FutureBuilder>( + future: _flightsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text( + '${l10n.errorLoadingFlights}: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Text( + l10n.noFlightsYet, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(color: Colors.white70), + ), + ); + } else { + final flights = snapshot.data!; + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: flights.length, + itemBuilder: (context, index) { + final flight = flights[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + leading: const Icon(Icons.flight_takeoff, + color: Colors.orangeAccent, size: 32), + title: Text( + '${flight.name} - ${DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000).toLocal().toString().split(' ')[0]}', + style: Theme.of(context).textTheme.titleMedium, + ), + trailing: const Icon(Icons.arrow_forward_ios, + size: 20, color: Colors.white54), + onTap: () { + + Navigator.of(context).push( + SlideRightPageRoute( + page: FlightDetailPage(flight: flight), + ), + ).then((_) => _loadData()); + }, + ), + ); + }, + ); + } + }, + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + + Navigator.of(context).push( + SlideUpPageRoute( + page: const NewFlightPage(), + ), + ).then((_) => _loadData()); + }, + label: Text( + l10n.newFlight, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + icon: const Icon(Icons.add_to_photos, size: 28), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } +} diff --git a/lib/pages/new_battery_page.dart b/lib/pages/new_battery_page.dart new file mode 100644 index 0000000..1663d76 --- /dev/null +++ b/lib/pages/new_battery_page.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:rtime/images_manager.dart'; +import 'package:image_picker/image_picker.dart'; + +class NewBatteryPage extends StatefulWidget { + const NewBatteryPage({super.key}); + + @override + State createState() => _NewBatteryPageState(); +} + +class _NewBatteryPageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _typeController = TextEditingController(); + final TextEditingController _voltageController = TextEditingController(); + String? _selectedImageUuid; + Image? _displayImage; + + @override + void dispose() { + _nameController.dispose(); + _typeController.dispose(); + _voltageController.dispose(); + super.dispose(); + } + + + Future _pickImage(ImageSource source) async { + final l10n = AppLocalizations.of(context)!; + final uuid = await ImagesManager.instance.createImage(source); + if (uuid != null) { + final loadedImage = await ImagesManager.instance.loadImage(uuid); + setState(() { + _selectedImageUuid = uuid; + _displayImage = loadedImage; + }); + if (mounted) { + + + + } + } else { + if (mounted) { + + + + } + } + } + + void _saveBattery() async { + if (_formKey.currentState!.validate()) { + final l10n = AppLocalizations.of(context)!; + final newBattery = Battery( + name: _nameController.text.trim(), + type: _typeController.text.trim(), + voltage: double.parse(_voltageController.text.trim()), + imageUuid: _selectedImageUuid, + ); + + try { + await DbHelper.instance.insertBattery(newBattery); + if (mounted) { + + + + Navigator.of(context).pop(); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(l10n.addBattery), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + + Center( + child: Column( + children: [ + Container( + width: 150, + height: 150, + decoration: BoxDecoration( + color: Colors.blueGrey[800], + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.lightBlueAccent, width: 2), + + image: _displayImage != null + ? DecorationImage( + image: _displayImage!.image, + fit: BoxFit.cover, + ) + : null, + ), + + child: _displayImage == null + ? Icon( + Icons.camera_alt, + size: 60, + color: Colors.blueGrey[400], + ) + : null, + ), + const SizedBox(height: 10), + Text(l10n.batteryImage, + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: Text(l10n.takePhoto), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + const SizedBox(width: 10), + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.gallery), + icon: const Icon(Icons.photo_library), + label: Text(l10n.chooseFromGallery), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 30), + TextFormField( + controller: _nameController, + decoration: InputDecoration( + labelText: l10n.batteryName, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryName; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _typeController, + decoration: InputDecoration( + labelText: l10n.batteryType, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryType; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _voltageController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: l10n.batteryVoltage, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterBatteryVoltage; + } + if (double.tryParse(value) == null) { + return l10n.pleaseEnterValidNumber; + } + return null; + }, + ), + const SizedBox(height: 30), + Center( + child: ElevatedButton( + onPressed: _saveBattery, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + l10n.saveBattery, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/new_drone_page.dart b/lib/pages/new_drone_page.dart new file mode 100644 index 0000000..948ce73 --- /dev/null +++ b/lib/pages/new_drone_page.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'package:rtime/images_manager.dart'; +import 'package:image_picker/image_picker.dart'; + +class NewDronePage extends StatefulWidget { + const NewDronePage({super.key}); + + @override + State createState() => _NewDronePageState(); +} + +class _NewDronePageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _nameController = TextEditingController(); + String? _selectedImageUuid; + Image? _displayImage; + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + + Future _pickImage(ImageSource source) async { + final l10n = AppLocalizations.of(context)!; + final uuid = await ImagesManager.instance.createImage(source); + if (uuid != null) { + final loadedImage = await ImagesManager.instance.loadImage(uuid); + setState(() { + _selectedImageUuid = uuid; + _displayImage = loadedImage; + }); + if (mounted) { + + + + } + } else { + if (mounted) { + + + + } + } + } + + void _saveDrone() async { + if (_formKey.currentState!.validate()) { + final l10n = AppLocalizations.of(context)!; + final newDrone = Drone( + name: _nameController.text.trim(), + imageUuid: _selectedImageUuid, + ); + + try { + await DbHelper.instance.insertDrone(newDrone); + if (mounted) { + + + + Navigator.of(context).pop(); + } + } catch (e) { + if (mounted) { + + + + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(l10n.addDrone), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + + Center( + child: Column( + children: [ + Container( + width: 150, + height: 150, + decoration: BoxDecoration( + color: Colors.blueGrey[800], + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.lightBlueAccent, width: 2), + + image: _displayImage != null + ? DecorationImage( + image: _displayImage!.image, + fit: BoxFit.cover, + ) + : null, + ), + + child: _displayImage == null + ? Icon( + Icons.camera_alt, + size: 60, + color: Colors.blueGrey[400], + ) + : null, + ), + const SizedBox(height: 10), + Text(l10n.droneImage, + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.camera), + icon: const Icon(Icons.camera_alt), + label: Text(l10n.takePhoto), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + const SizedBox(width: 10), + ElevatedButton.icon( + onPressed: () => _pickImage(ImageSource.gallery), + icon: const Icon(Icons.photo_library), + label: Text(l10n.chooseFromGallery), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[700], + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 30), + TextFormField( + controller: _nameController, + decoration: InputDecoration( + labelText: l10n.droneName, + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white30), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent), + ), + ), + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.pleaseEnterDroneName; + } + return null; + }, + ), + const SizedBox(height: 30), + Center( + child: ElevatedButton( + onPressed: _saveDrone, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + l10n.saveDrone, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/new_flight_page.dart b/lib/pages/new_flight_page.dart new file mode 100644 index 0000000..5508b06 --- /dev/null +++ b/lib/pages/new_flight_page.dart @@ -0,0 +1,578 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/models/flight.dart'; +import 'package:rtime/db/db_helper.dart'; +import 'dart:async'; +import 'package:geolocator/geolocator.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +class NewFlightPage extends StatefulWidget { + const NewFlightPage({super.key}); + + @override + State createState() => _NewFlightPageState(); +} + +class _NewFlightPageState extends State { + + late Future> _dronesFuture; + late Future> _batteriesFuture; + + + Drone? selectedDrone; + Battery? selectedBattery; + bool isFlightActive = false; + Stopwatch stopwatch = Stopwatch(); + Timer? timer; + String formattedTime = '00:00:00'; + int? flightStartTime; + + + bool useGpsLocation = false; + Position? currentPosition; + bool isGettingLocation = false; + + + final MapController _mapController = MapController(); + + + bool _initialMapCentered = false; + + @override + void initState() { + super.initState(); + _loadAvailableItems(); + } + + + void _loadAvailableItems() { + _dronesFuture = DbHelper.instance.getDrones(); + _batteriesFuture = DbHelper.instance.getBatteries(); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + + + Future _getCurrentLocation() async { + if (!mounted) return; + + setState(() { + isGettingLocation = true; + }); + + final l10n = AppLocalizations.of(context)!; + + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + if (mounted) { + + } + if (mounted) { + setState(() { + useGpsLocation = false; + }); + } + if (mounted) { + setState(() { + isGettingLocation = false; + }); + } + return; + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + if (mounted) { + + } + if (mounted) { + setState(() { + useGpsLocation = false; + }); + } + if (mounted) { + setState(() { + isGettingLocation = false; + }); + } + return; + } + } + + if (permission == LocationPermission.deniedForever) { + if (mounted) { + + } + if (mounted) { + setState(() { + useGpsLocation = false; + }); + } + if (mounted) { + setState(() { + isGettingLocation = false; + }); + } + return; + } + + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + timeLimit: const Duration(seconds: 15), + ); + if (mounted) { + setState(() { + currentPosition = position; + + + if (_initialMapCentered && _mapController.camera.center != LatLng(position.latitude, position.longitude)) { + _mapController.move( + LatLng(currentPosition!.latitude, currentPosition!.longitude), + _mapController.camera.zoom, + ); + } + }); + } + if (mounted) { + + } + } catch (e) { + if (mounted) { + + } + if (mounted) { + setState(() { + useGpsLocation = false; + currentPosition = null; + }); + } + } finally { + if (mounted) { + setState(() { + isGettingLocation = false; + }); + } + } + } + + + void _startFlight() { + final l10n = AppLocalizations.of(context)!; + + if (selectedDrone == null || selectedBattery == null) { + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.selectDroneBattery)), + ); + return; + } + + if (useGpsLocation) { + setState(() { + isGettingLocation = true; + }); + + _getCurrentLocation().then((_) { + + if (useGpsLocation && currentPosition == null) { + + + return; + } + + _proceedToStartFlight(); + }); + } else { + _proceedToStartFlight(); + } + } + + + void _proceedToStartFlight() { + final l10n = AppLocalizations.of(context)!; + + setState(() { + isFlightActive = true; + stopwatch.start(); + flightStartTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; + timer = Timer.periodic(const Duration(milliseconds: 100), (Timer t) { + if (stopwatch.isRunning) { + setState(() { + formattedTime = _formatDuration(stopwatch.elapsed); + }); + } + }); + }); + + } + + + Future _stopFlight() async { + final l10n = AppLocalizations.of(context)!; + + + stopwatch.stop(); + timer?.cancel(); + + + final int flightEndTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + + final Duration flightDuration = stopwatch.elapsed; + + + final Flight newFlight = Flight( + name: '${selectedDrone!.name} - ${DateTime.fromMillisecondsSinceEpoch(flightStartTime! * 1000).toLocal().toString().split(' ')[0]}', + startTimestamp: flightStartTime!, + endTimestamp: flightEndTime, + droneId: selectedDrone!.id!, + batteryId: selectedBattery!.id!, + locationLat: currentPosition?.latitude, + locationLong: currentPosition?.longitude, + ); + + try { + + await DbHelper.instance.insertFlight(newFlight); + if (mounted) { + + } + } catch (e) { + if (mounted) { + + } + } finally { + + setState(() { + isFlightActive = false; + stopwatch.reset(); + formattedTime = '00:00:00'; + selectedDrone = null; + selectedBattery = null; + currentPosition = null; + useGpsLocation = false; + isGettingLocation = false; + _initialMapCentered = false; + }); + + if (mounted) { + Navigator.pop(context); + } + } + } + + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String hours = twoDigits(duration.inHours); + String minutes = twoDigits(duration.inMinutes.remainder(60)); + String seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$hours:$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + appBar: AppBar( + title: Text(l10n.newFlight), + backgroundColor: Colors.transparent, + elevation: 0, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text( + l10n.chooseDrone, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 15), + Card( + margin: const EdgeInsets.only(bottom: 20), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: FutureBuilder>( + future: _dronesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('${l10n.errorLoadingDrones}: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Text( + l10n.noDronesYet, + style: Theme.of(context).textTheme.titleSmall, + ), + ); + } else { + final drones = snapshot.data!; + return DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: selectedDrone, + hint: Text( + l10n.selectDroneHint, + style: Theme.of(context).textTheme.bodyLarge, + ), + dropdownColor: Theme.of(context).cardTheme.color, + style: Theme.of(context).textTheme.titleMedium, + icon: const Icon(Icons.arrow_drop_down, color: Colors.white70), + onChanged: isFlightActive || isGettingLocation + ? null + : (Drone? newValue) { + setState(() { + selectedDrone = newValue; + }); + }, + items: drones.map>((Drone drone) { + return DropdownMenuItem( + value: drone, + child: Text(drone.name), + ); + }).toList(), + ), + ); + } + }, + ), + ), + ), + + + Text( + l10n.chooseBattery, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 15), + Card( + margin: const EdgeInsets.only(bottom: 30), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: FutureBuilder>( + future: _batteriesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('${l10n.errorLoadingBatteries}: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Text( + l10n.noBatteriesYet, + style: Theme.of(context).textTheme.titleSmall, + ), + ); + } else { + final batteries = snapshot.data!; + return DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: selectedBattery, + hint: Text( + l10n.selectBatteryHint, + style: Theme.of(context).textTheme.bodyLarge, + ), + dropdownColor: Theme.of(context).cardTheme.color, + style: Theme.of(context).textTheme.titleMedium, + icon: const Icon(Icons.arrow_drop_down, color: Colors.white70), + onChanged: isFlightActive || isGettingLocation + ? null + : (Battery? newValue) { + setState(() { + selectedBattery = newValue; + }); + }, + items: batteries.map>((Battery battery) { + return DropdownMenuItem( + value: battery, + child: Text('${battery.name} (${battery.voltage}V)'), + ); + }).toList(), + ), + ); + } + }, + ), + ), + ), + + + Card( + margin: const EdgeInsets.only(bottom: 30), + child: ListTile( + title: Text(l10n.recordFlightLocation, style: Theme.of(context).textTheme.titleMedium), + trailing: Switch( + value: useGpsLocation, + onChanged: isFlightActive || isGettingLocation + ? null + : (bool value) async { + setState(() { + useGpsLocation = value; + }); + if (value) { + await _getCurrentLocation(); + } else { + setState(() { + currentPosition = null; + _initialMapCentered = false; + }); + } + }, + activeColor: Theme.of(context).colorScheme.primary, + ), + leading: Icon(Icons.location_on, color: Theme.of(context).colorScheme.secondary), + ), + ), + + + if (currentPosition != null && useGpsLocation) + Container( + margin: const EdgeInsets.only(bottom: 20), + height: 250, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: ClipRRect( + + borderRadius: BorderRadius.circular(15), + child: FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(currentPosition!.latitude, currentPosition!.longitude), + initialZoom: 17.0, + maxZoom: 19.0, + minZoom: 3.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + onMapReady: () { + if (!_initialMapCentered && currentPosition != null) { + _mapController.move( + LatLng(currentPosition!.latitude, currentPosition!.longitude), + _mapController.camera.zoom, + ); + setState(() { + _initialMapCentered = true; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.rtime', + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(currentPosition!.latitude, currentPosition!.longitude), + width: 80, + height: 80, + child: const Icon( + Icons.location_on, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], + ), + ), + ), + + + Center( + child: Text( + formattedTime, + style: const TextStyle( + fontSize: 72, + fontWeight: FontWeight.bold, + color: Colors.lightBlueAccent, + fontFamily: 'RobotoMono', + ), + ), + ), + const SizedBox(height: 40), + + + Center( + child: isFlightActive + ? ElevatedButton.icon( + onPressed: _stopFlight, + icon: const Icon(Icons.stop, size: 30), + label: Text( + l10n.stopFlight, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + minimumSize: const Size(250, 70), + ), + ) + : (isGettingLocation + ? Column( + children: [ + const CircularProgressIndicator(color: Colors.tealAccent), + const SizedBox(height: 10), + Text(l10n.obtainingLocation, style: Theme.of(context).textTheme.titleSmall), + ], + ) + : ElevatedButton.icon( + onPressed: _startFlight, + icon: const Icon(Icons.flight_takeoff, size: 30), + label: Text( + l10n.startFlight, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.tealAccent, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + minimumSize: const Size(250, 70), + ), + )), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..9ac277e --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:rtime/providers/local_provider.dart'; +import 'package:rtime/providers/theme_provider.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final currentLocale = Localizations.localeOf(context); + final themeProvider = Provider.of(context); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + title: Text(l10n.settingsTitle), + backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + foregroundColor: Theme.of(context).appBarTheme.foregroundColor, + iconTheme: Theme.of(context).iconTheme, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.languageSetting, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + Card( + color: Theme.of(context).cardTheme.color, + child: Column( + children: [ + ListTile( + title: Text(l10n.english, style: Theme.of(context).textTheme.titleMedium), + trailing: currentLocale.languageCode == 'en' + ? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + final localeProvider = Provider.of(context, listen: false); + localeProvider.setLocale(const Locale('en', '')); + + + + }, + ), + ListTile( + title: Text(l10n.french, style: Theme.of(context).textTheme.titleMedium), + trailing: currentLocale.languageCode == 'fr' + ? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + final localeProvider = Provider.of(context, listen: false); + localeProvider.setLocale(const Locale('fr', '')); + + + + }, + ), + ], + ), + ), + const SizedBox(height: 30), + Text( + l10n.themeSetting, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + Card( + color: Theme.of(context).cardTheme.color, + child: Column( + children: [ + ListTile( + title: Text(l10n.themeSystem, style: Theme.of(context).textTheme.titleMedium), + trailing: themeProvider.themeMode == ThemeMode.system + ? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + themeProvider.setThemeMode(ThemeMode.system); + + + + }, + ), + ListTile( + title: Text(l10n.themeLight, style: Theme.of(context).textTheme.titleMedium), + trailing: themeProvider.themeMode == ThemeMode.light + ? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + themeProvider.setThemeMode(ThemeMode.light); + + + + }, + ), + ListTile( + title: Text(l10n.themeDark, style: Theme.of(context).textTheme.titleMedium), + trailing: themeProvider.themeMode == ThemeMode.dark + ? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + themeProvider.setThemeMode(ThemeMode.dark); + + + + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/providers/local_provider.dart b/lib/providers/local_provider.dart new file mode 100644 index 0000000..c47d349 --- /dev/null +++ b/lib/providers/local_provider.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LocaleProvider extends ChangeNotifier { + Locale? _locale; + + Locale? get locale => _locale; + + + LocaleProvider() { + _loadLocale(); + } + + + void _loadLocale() async { + final prefs = await SharedPreferences.getInstance(); + final languageCode = prefs.getString('languageCode'); + final countryCode = prefs.getString('countryCode'); + + if (languageCode != null) { + _locale = Locale(languageCode, countryCode); + } else { + _locale = null; + } + notifyListeners(); + } + + + void setLocale(Locale newLocale) async { + if (_locale == newLocale) return; + + _locale = newLocale; + notifyListeners(); + + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('languageCode', newLocale.languageCode); + if (newLocale.countryCode != null) { + await prefs.setString('countryCode', newLocale.countryCode!); + } else { + await prefs.remove('countryCode'); + } + } + + + void clearLocale() async { + _locale = null; + notifyListeners(); + + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('languageCode'); + await prefs.remove('countryCode'); + } +} diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart new file mode 100644 index 0000000..01de855 --- /dev/null +++ b/lib/providers/theme_provider.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeProvider extends ChangeNotifier { + ThemeMode _themeMode = ThemeMode.system; + + ThemeMode get themeMode => _themeMode; + + ThemeProvider() { + _loadThemeMode(); + } + + void _loadThemeMode() async { + final prefs = await SharedPreferences.getInstance(); + final themeIndex = prefs.getInt('themeMode') ?? 0; + _themeMode = ThemeMode.values[themeIndex]; + notifyListeners(); + } + + void setThemeMode(ThemeMode mode) async { + if (_themeMode != mode) { + _themeMode = mode; + notifyListeners(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('themeMode', mode.index); + } + } +} diff --git a/lib/removecom.py b/lib/removecom.py new file mode 100644 index 0000000..1a9058f --- /dev/null +++ b/lib/removecom.py @@ -0,0 +1,83 @@ +import os +import sys +import re + +def clean_comments_in_file(filepath): + """ + Supprime le reste de la ligne à partir de '//' dans un fichier donné, + en essayant d'éviter les URL. Gère aussi les commentaires /* ... */. + """ + try: + with open(filepath, 'r', encoding='utf-8') as f_read: + content = f_read.read() + + # Première passe : supprimer les commentaires /* ... */ + # Utilise re.DOTALL pour que . corresponde aussi aux retours à la ligne + # et non-greedy quantifier *? + content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) + + cleaned_lines = [] + for line in content.splitlines(): + # Vérifie si la ligne contient une URL avant de chercher un commentaire + if re.search(r'https?://', line): + # Si une URL est trouvée, nous sommes très prudents. + # Nous ne supprimons les commentaires que s'ils sont CLAIREMENT après une URL et non mélangés + # Cela reste une heuristique et n'est pas parfait. + comment_index = line.find('//') + if comment_index != -1 and not re.search(r'["\']https?://.*?//', line): # Évite les // dans les URLs entre guillemets + # On tente de voir si le commentaire est vraiment à la fin de la ligne, + # après d'éventuelles guillemets. + # Ceci est une simplification et peut encore échouer. + if comment_index > line.rfind('"') and comment_index > line.rfind("'"): + cleaned_lines.append(line[:comment_index].rstrip()) + else: + cleaned_lines.append(line) # Conserve la ligne si le // est potentiellement dans une chaîne/URL + else: + cleaned_lines.append(line) + else: + # Si pas d'URL, on peut être plus agressif avec les // + if '//' in line: + index = line.find('//') + cleaned_lines.append(line[:index].rstrip()) + else: + cleaned_lines.append(line) + + # Rejoindre les lignes avec des retours à la ligne. + # Ajoute un retour à la ligne final si le fichier en avait un. + final_content = '\n'.join(cleaned_lines) + if content.endswith('\n') and not final_content.endswith('\n'): + final_content += '\n' + + with open(filepath, 'w', encoding='utf-8') as f_write: + f_write.write(final_content) + print(f"Commentaires nettoyés dans : {filepath}") + except Exception as e: + print(f"Erreur lors du traitement de {filepath} : {e}") + +def main(): + if len(sys.argv) < 2: + print("Usage: python clean_comments.py ") + print("Exemple: python clean_comments.py ./mon_projet_flutter") + sys.exit(1) + + target_directory = sys.argv[1] + + if not os.path.isdir(target_directory): + print(f"Erreur : Le chemin '{target_directory}' n'est pas un répertoire valide.") + sys.exit(1) + + print(f"Nettoyage des commentaires '//' et '/* ... */' dans le répertoire : {target_directory}") + print("-" * 70) + + for root, _, files in os.walk(target_directory): + for file in files: + filepath = os.path.join(root, file) + # Filtrez les types de fichiers que vous souhaitez traiter + if filepath.endswith(('.dart', '.js', '.ts', '.java', '.cpp', '.c', '.h', '.py')): + clean_comments_in_file(filepath) + + print("-" * 70) + print("Nettoyage terminé.") + +if __name__ == "__main__": + main() diff --git a/lib/widgets/battery_card.dart b/lib/widgets/battery_card.dart new file mode 100644 index 0000000..d7bd178 --- /dev/null +++ b/lib/widgets/battery_card.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/models/battery.dart'; +import 'package:rtime/images_manager.dart'; + +class BatteryCard extends StatelessWidget { + final Battery battery; + final VoidCallback? onTap; + + const BatteryCard({ + super.key, + required this.battery, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final BorderRadius cardBorderRadius = + Theme.of(context).cardTheme.shape is RoundedRectangleBorder + ? (Theme.of(context).cardTheme.shape as RoundedRectangleBorder).borderRadius as BorderRadius + : BorderRadius.circular(12); + + return Card( + margin: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: onTap, + borderRadius: cardBorderRadius, + child: ClipRRect( + borderRadius: cardBorderRadius, + child: SizedBox( + width: 140, + height: 170, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.blueGrey[700] + : Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: battery.imageUuid != null && battery.imageUuid!.isNotEmpty + ? FutureBuilder( + future: ImagesManager.instance.loadImage(battery.imageUuid!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator(color: Colors.tealAccent)); + } else if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { + return Icon(Icons.broken_image, size: 50, color: Colors.blueGrey[400]); + } else { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: snapshot.data!, + ); + } + }, + ) + : Icon(Icons.battery_charging_full, size: 50, color: Theme.of(context).brightness == Brightness.dark ? Colors.tealAccent : Colors.green.shade400), + ), + const SizedBox(height: 10), + Text( + battery.name, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '${battery.voltage}V - ${battery.type}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).brightness == Brightness.dark ? Colors.white70 : Colors.black54), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/drone_cart.dart b/lib/widgets/drone_cart.dart new file mode 100644 index 0000000..0ccf5fa --- /dev/null +++ b/lib/widgets/drone_cart.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:rtime/models/drone.dart'; +import 'package:rtime/images_manager.dart'; + +class DroneCard extends StatelessWidget { + final Drone drone; + final VoidCallback? onTap; + + const DroneCard({ + super.key, + required this.drone, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + + final BorderRadius cardBorderRadius = + Theme.of(context).cardTheme.shape is RoundedRectangleBorder + ? (Theme.of(context).cardTheme.shape as RoundedRectangleBorder).borderRadius as BorderRadius + : BorderRadius.circular(12); + + return Card( + margin: const EdgeInsets.only(right: 16), + + child: InkWell( + onTap: onTap, + + borderRadius: cardBorderRadius, + child: ClipRRect( + borderRadius: cardBorderRadius, + child: SizedBox( + width: 140, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.blueGrey[700] + : Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: drone.imageUuid != null && drone.imageUuid!.isNotEmpty + ? FutureBuilder( + future: ImagesManager.instance.loadImage(drone.imageUuid!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator(color: Colors.lightBlueAccent)); + } else if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { + return Icon(Icons.broken_image, size: 50, color: Colors.blueGrey[400]); + } else { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: snapshot.data!, + ); + } + }, + ) + : Icon(Icons.airplanemode_active, size: 50, color: Theme.of(context).brightness == Brightness.dark ? Colors.lightBlueAccent : Colors.blue.shade400), + ), + const SizedBox(height: 10), + Text( + drone.name, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/page_transition_animations.dart b/lib/widgets/page_transition_animations.dart new file mode 100644 index 0000000..3c28d4b --- /dev/null +++ b/lib/widgets/page_transition_animations.dart @@ -0,0 +1,88 @@ + +import 'package:flutter/material.dart'; + + +class FadePageRoute extends PageRouteBuilder { + final Widget page; + FadePageRoute({required this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + FadeTransition( + opacity: animation, + child: child, + ), + ); +} + + +class SlideRightPageRoute extends PageRouteBuilder { + final Widget page; + SlideRightPageRoute({required this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.ease; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ); +} + + +class SlideUpPageRoute extends PageRouteBuilder { + final Widget page; + SlideUpPageRoute({required this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + const begin = Offset(0.0, 1.0); + const end = Offset.zero; + const curve = Curves.easeOut; + + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ); +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a41ae29..b6e95a2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,17 @@ import FlutterMacOS import Foundation import file_selector_macos +import geolocator_apple import path_provider_foundation +import shared_preferences_foundation import sqflite_darwin import sqlite3_flutter_libs func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 96c1f29..f7d6533 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" dart_style: dependency: transitive description: @@ -270,6 +278,19 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a + url: "https://pub.dev" + source: hosted + version: "8.1.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -296,6 +317,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "149876cc5207a0f5daf4fdd3bfcf0a0f27258b3fe95108fa084f527ad0568f1b" + url: "https://pub.dev" + source: hosted + version: "12.0.0" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" glob: dependency: transitive description: @@ -432,6 +501,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -464,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.9.5" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" leak_tracker: dependency: transitive description: @@ -496,6 +581,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" + url: "https://pub.dev" + source: hosted + version: "2.6.0" logging: dependency: "direct main" description: @@ -528,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -536,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -624,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + polylabel: + dependency: transitive + description: + name: polylabel + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" pool: dependency: transitive description: @@ -640,6 +765,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" pub_semver: dependency: transitive description: @@ -656,6 +797,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -845,6 +1042,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" uuid: dependency: "direct main" description: @@ -901,6 +1106,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 938d294..f5712b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,14 @@ dependencies: uuid: ^4.5.1 json_serializable: ^6.9.5 image_cropper: ^9.1.0 + intl: ^0.20.2 + provider: ^6.1.2 + geolocator: ^12.0.0 + flutter_map: ^8.0.0 + latlong2: ^0.9.1 + flutter_localizations: + sdk: flutter + shared_preferences: ^2.2.0 dev_dependencies: flutter_test: @@ -67,6 +75,7 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + generate: true # To add assets to your application, add an assets section, like this: # assets: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 987fb3d..6455bc4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 3057813..64adc44 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + geolocator_windows sqlite3_flutter_libs )