2 Commits
main ... temp

Author SHA1 Message Date
25f3eb02de Remove uncessary file 2025-07-09 12:42:45 +02:00
1b261c08bb Link between front-end and back-end 2025-07-09 12:41:00 +02:00
30 changed files with 5424 additions and 40 deletions

View File

@ -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

View File

@ -49,4 +49,8 @@
<!-- GPS Location permission to be able to locate flights. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<!-- Camera/storage permission to be able to take picture of the drone/battery -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>

View File

@ -47,5 +47,11 @@
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>To locate the flights.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to your camera to take photos for your drones/batteries.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo gallery to select images for your drones/batteries.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not use the microphone.</string>
</dict>
</plist>

View File

@ -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<int> updateDrone(Drone drone) async {
final db = await database;
return await db.update(
"drones",
drone.toMap(),
where: "id = ?",
whereArgs: [drone.id],
);
}
Future<int> 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<int> updateBattery(Battery battery) async {
final db = await database;
return await db.update(
"batteries",
battery.toMap(),
where: "id = ?",
whereArgs: [battery.id],
);
}
Future<int> deleteBattery(int batteryId) async {
// TODO: Delete image
final db = await database;

View File

@ -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<String?> 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<bool> 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;
}
}
}

210
lib/l10n/app_en.arb Normal file
View File

@ -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"
}

210
lib/l10n/app_fr.arb Normal file
View File

@ -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"
}

View File

@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>['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.'
);
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Text("RtimeAPP");
}
return Consumer2<LocaleProvider, ThemeProvider>(
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(),
);
},
);
}
}

View File

@ -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"],
);

View File

@ -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<BatteryDetailPage> createState() => _BatteryDetailPageState();
}
class _BatteryDetailPageState extends State<BatteryDetailPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _typeController;
late TextEditingController _voltageController;
String? _currentImageUuid;
Image? _displayImage;
bool _isEditing = false;
List<Flight> _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<void> _loadImage() async {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!);
setState(() {
_displayImage = loadedImage;
});
} else {
setState(() {
_displayImage = null;
});
}
}
Future<void> _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<void> _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<void> _deleteImage() async {
if (_currentImageUuid != null) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
setState(() {
_currentImageUuid = null;
_displayImage = null;
});
}
}
Future<void> _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<void> _deleteBattery() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
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());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -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<DroneDetailPage> createState() => _DroneDetailPageState();
}
class _DroneDetailPageState extends State<DroneDetailPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
String? _currentImageUuid;
Image? _displayImage;
bool _isEditing = false;
List<Flight> _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<void> _loadImage() async {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!);
setState(() {
_displayImage = loadedImage;
});
} else {
setState(() {
_displayImage = null;
});
}
}
Future<void> _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<void> _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<void> _deleteImage() async {
if (_currentImageUuid != null) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
setState(() {
_currentImageUuid = null;
_displayImage = null;
});
}
}
Future<void> _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<void> _deleteDrone() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
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());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -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<FlightDetailPage> createState() => _FlightDetailPageState();
}
class _FlightDetailPageState extends State<FlightDetailPage> {
Drone? _associatedDrone;
Battery? _associatedBattery;
Image? _droneDisplayImage;
Image? _batteryDisplayImage;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadAssociatedData();
}
Future<void> _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<void> _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<void> _deleteFlight() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
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,
),
),
),
],
),
),
],
),
);
}
}

389
lib/pages/home_page.dart Normal file
View File

@ -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<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<List<Drone>> _dronesFuture;
late Future<List<Battery>> _batteriesFuture;
late Future<List<Flight>> _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: <Widget>[
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<List<Drone>>(
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<List<Battery>>(
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<List<Flight>>(
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,
);
}
}

View File

@ -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<NewBatteryPage> createState() => _NewBatteryPageState();
}
class _NewBatteryPageState extends State<NewBatteryPage> {
final _formKey = GlobalKey<FormState>();
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<void> _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),
),
),
),
],
),
),
),
);
}
}

View File

@ -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<NewDronePage> createState() => _NewDronePageState();
}
class _NewDronePageState extends State<NewDronePage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
String? _selectedImageUuid;
Image? _displayImage;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _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),
),
),
),
],
),
),
),
);
}
}

View File

@ -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<NewFlightPage> createState() => _NewFlightPageState();
}
class _NewFlightPageState extends State<NewFlightPage> {
late Future<List<Drone>> _dronesFuture;
late Future<List<Battery>> _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<void> _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<void> _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: <Widget>[
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<List<Drone>>(
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<Drone>(
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<DropdownMenuItem<Drone>>((Drone drone) {
return DropdownMenuItem<Drone>(
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<List<Battery>>(
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<Battery>(
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<DropdownMenuItem<Battery>>((Battery battery) {
return DropdownMenuItem<Battery>(
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),
],
),
),
);
}
}

View File

@ -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<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final currentLocale = Localizations.localeOf(context);
final themeProvider = Provider.of<ThemeProvider>(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<LocaleProvider>(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<LocaleProvider>(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);
},
),
],
),
),
],
),
),
);
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}
}

View File

@ -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<Image?>(
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,
),
],
),
),
),
),
),
);
}
}

View File

@ -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<Image?>(
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,
),
],
),
),
),
),
),
);
}
}

View File

@ -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<double> animation,
Animation<double> secondaryAnimation,
) =>
page,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>
FadeTransition(
opacity: animation,
child: child,
),
);
}
class SlideRightPageRoute extends PageRouteBuilder {
final Widget page;
SlideRightPageRoute({required this.page})
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
page,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> 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<double> animation,
Animation<double> secondaryAnimation,
) =>
page,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> 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,
);
},
);
}

View File

@ -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"))
}

View File

@ -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:

View File

@ -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:

View File

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <geolocator_windows/geolocator_windows.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
geolocator_windows
sqlite3_flutter_libs
)