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
)