8 Commits
main ... temp2

62 changed files with 5774 additions and 48 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

@ -2,7 +2,7 @@
<application
android:label="rtime"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/launcher_icon">
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

7
assets/images/drone.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g><g><path fill="#000000" d="M46.4,10.5c-9.4,1.8-16.9,5.8-23.7,12.7c-6,6-9.8,12.8-11.9,21.1c-1.1,4.4-1.1,15.4,0,19.8c2.2,8.6,5.9,15.2,12.3,21.5c4.7,4.6,9.2,7.6,14.8,9.9c5.7,2.2,8.9,2.8,16.1,2.8c8.4,0,10.3-0.4,20.6-4.7c0.6-0.2,8.7,10.7,10,13.7c1,2,1,2.8,1,20.8c0,18,0,18.8-1,20.8c-1.3,3-9.4,13.9-10,13.7c-10.3-4.2-12.2-4.7-20.6-4.7c-9.4,0-15.9,1.8-23.6,6.6c-4.4,2.7-11.2,9.5-13.9,13.9c-4.8,7.7-6.6,14.1-6.6,23.6c0,7.4,0.6,10.5,3,16.6c4.3,10.9,13.9,20.4,24.9,24.7c5.7,2.2,8.9,2.8,16.1,2.8c9.4,0,15.9-1.8,23.6-6.6c4.4-2.7,11.2-9.5,13.9-13.9c4.8-7.7,6.6-14.1,6.6-23.6c0-8.4-0.4-10.3-4.7-20.6c-0.2-0.6,10.6-8.6,13.4-9.9c2.8-1.2,6.8-1.8,10.9-1.3c4.4,0.5,15.9,0.5,20.3,0c4.1-0.4,8.1,0.1,10.9,1.3c1.1,0.5,4.7,2.8,7.8,5.1c4.5,3.3,5.7,4.4,5.4,5c-3.7,8.5-5,15-4.7,22.4c0.6,11.7,4.9,21.2,13.4,29.6c6,5.9,12.7,9.6,20.9,11.6c4.4,1.1,15.4,1.1,19.8,0c8.4-2.1,15.1-5.9,21.3-12.1c6.1-6.2,9.9-12.9,12.1-21.3c1.1-4.4,1.1-15.4,0-19.8c-2.1-8.2-5.8-14.9-11.6-20.9c-8.4-8.5-17.8-12.8-29.6-13.4c-7.5-0.4-14,1-22.4,4.7c-0.6,0.3-1.7-1-5-5.4c-2.3-3.1-4.6-6.7-5.1-7.8c-1.2-2.8-1.7-6.8-1.3-11.2c0.5-4.8,0.5-15.1,0-19.9c-0.4-4.4,0.1-8.4,1.3-11.2c1.2-2.8,9.2-13.6,9.9-13.4c10.3,4.2,12.2,4.7,20.6,4.7c9.4,0,15.9-1.8,23.6-6.6c4.4-2.7,11.2-9.5,13.9-13.9c4.8-7.7,6.6-14.1,6.6-23.6c0-9.3-1.5-14.8-6.1-22.9c-2.7-4.7-9.6-11.7-14.4-14.6c-7.7-4.8-14.1-6.6-23.6-6.6s-15.9,1.8-23.6,6.6c-4.4,2.7-11.2,9.5-13.9,13.9c-4.8,7.7-6.6,14.1-6.6,23.6c0,8.4,0.4,10.3,4.7,20.6c0.2,0.6-10.7,8.7-13.7,10c-2,0.9-2.9,1-20.8,1s-18.8,0-20.8-1c-3-1.3-13.9-9.4-13.7-10c4.2-10.3,4.7-12.2,4.7-20.6c0-7.2-0.6-10.4-2.8-16.1c-2.2-5.5-5.3-10.2-9.7-14.6c-6.2-6.3-12.6-10.1-21-12.2C60.2,9.9,51,9.6,46.4,10.5z M62.9,18.3c4.6,1.3,7.8,2.6,11.2,4.9C88.7,32.8,94.6,50.5,88.7,67c-0.9,2.5-1.6,3.7-1.9,3.5c-0.3-0.2-5.3-3.7-11-7.9c-9.2-6.7-10.5-7.8-10.5-8.8c0-0.7-0.2-1.9-0.5-2.7c-0.5-1.4-0.4-1.5,2.9-5c4.1-4.4,7.2-8.5,8.4-11.1c0.8-1.8,0.8-2.1,0.2-2.7c-0.6-0.6-0.9-0.6-2.7,0.2c-2.7,1.2-6.7,4.2-11.1,8.4c-3.5,3.3-3.6,3.4-5,2.9c-3.7-1.3-7.6-0.2-10.8,3c-3.2,3.2-4.2,7.1-3,10.8c0.5,1.4,0.4,1.5-2.9,5c-4.1,4.4-7.2,8.5-8.4,11.1c-0.8,1.8-0.8,2.1-0.2,2.7c0.6,0.6,0.9,0.6,2.7-0.2c2.7-1.2,6.7-4.2,11.1-8.4c3.5-3.3,3.6-3.4,5-2.9c0.8,0.3,2,0.5,2.7,0.5c1.1,0,2.2,1.3,8.8,10.5c4.2,5.8,7.8,10.8,7.9,11.1c0.2,0.3-1,1-3.5,1.9c-16.5,6-34.2,0.1-43.8-14.6c-8.8-13.4-7.6-31.4,2.8-43.7c5.5-6.5,12.6-10.7,21.2-12.5C50.9,17.1,59.3,17.3,62.9,18.3z M210.7,18.3c7,1.9,12.5,5,17.3,9.8c14.4,14.4,14.4,37.6,0,52.1c-8.6,8.6-21,12.5-32.6,10.3c-4.5-0.9-10.2-3-9.7-3.7c0.1-0.3,3.7-5.2,7.9-11c6.7-9.1,7.8-10.5,8.8-10.5c0.7,0,1.9-0.2,2.7-0.5c1.4-0.5,1.5-0.4,5,2.9c4.4,4.1,8.5,7.2,11.1,8.4c1.8,0.8,2.1,0.8,2.7,0.2s0.6-0.9-0.2-2.7c-1.2-2.7-4.2-6.7-8.4-11.1c-3.3-3.5-3.4-3.6-2.9-5c1.3-3.7,0.2-7.6-3-10.8c-3.2-3.2-7.1-4.2-10.8-3c-1.4,0.5-1.5,0.4-5-2.9c-4.4-4.1-8.5-7.2-11.1-8.4c-1.8-0.8-2.1-0.8-2.7-0.2s-0.6,0.9,0.2,2.7c1.2,2.7,4.2,6.7,8.4,11.1c3.3,3.5,3.4,3.6,2.9,5c-0.3,0.8-0.5,2-0.5,2.7c0,1.1-1.3,2.2-10.4,8.8c-5.8,4.2-10.7,7.7-11,7.9c-0.7,0.5-2.8-4.9-3.7-9.7c-2-10.6,1-21.9,8.1-30.3c5.5-6.5,12.6-10.7,21.2-12.5C198.7,17.1,207.1,17.3,210.7,18.3z M60.7,165.6c4.8,0.9,10.2,3,9.7,3.7c-0.2,0.3-3.7,5.3-7.9,11c-6.7,9.1-7.8,10.4-8.8,10.4c-0.7,0-1.9,0.2-2.7,0.5c-1.4,0.5-1.5,0.4-5-2.9c-4.4-4.1-8.5-7.2-11.1-8.4c-1.8-0.8-2.1-0.8-2.7-0.2s-0.6,0.9,0.2,2.7c1.2,2.7,4.2,6.7,8.4,11.1c3.3,3.5,3.4,3.6,2.9,5c-2.8,7.9,5.9,16.5,13.8,13.8c1.4-0.5,1.5-0.4,5,2.9c4.4,4.1,8.5,7.2,11.1,8.4c1.8,0.8,2.1,0.8,2.7,0.2c0.6-0.6,0.6-0.9-0.2-2.7c-1.2-2.7-4.2-6.7-8.4-11.1c-3.3-3.5-3.4-3.6-2.9-5c0.3-0.8,0.5-2,0.5-2.7c0-1.1,1.3-2.2,10.5-8.8c5.8-4.2,10.7-7.8,11-7.9c0.7-0.4,2.8,5.2,3.7,9.7c2.2,11.6-1.7,24-10.3,32.6c-12.3,12.3-31.6,14.4-46,4.9c-14.5-9.6-20.5-27.2-14.7-43.6C25.4,172.6,43.3,162.3,60.7,165.6z M210.7,166.1c7,1.9,12.5,5,17.3,9.8c12.1,12.1,14.3,30.8,5.4,45.3c-10.8,17.6-34.2,23-51.4,11.8c-14.7-9.6-20.6-27.3-14.6-43.8c0.9-2.5,1.6-3.7,1.9-3.5c0.3,0.2,5.3,3.7,11.1,7.9c9.2,6.7,10.5,7.8,10.5,8.8c0,0.7,0.2,1.9,0.5,2.7c0.5,1.4,0.4,1.5-3,5.2c-4,4.2-7.3,8.7-8.4,11.3c-0.6,1.5-0.6,1.8,0,2.4c0.6,0.6,0.9,0.6,2.7-0.2c2.7-1.2,6.7-4.2,11.1-8.4c3.5-3.3,3.6-3.4,5-2.9c3.7,1.3,7.6,0.2,10.8-3c3.2-3.2,4.2-7.1,3-10.8c-0.5-1.4-0.4-1.5,2.9-5c4.1-4.4,7.2-8.5,8.4-11.1c0.8-1.8,0.8-2.1,0.2-2.7c-0.6-0.6-0.9-0.6-2.4,0c-2.5,1.1-7,4.4-11.3,8.4c-3.6,3.4-3.8,3.5-5.2,3c-0.8-0.3-2-0.5-2.7-0.5c-1.1,0-2.2-1.3-8.8-10.5c-4.2-5.8-7.8-10.7-7.9-11c-0.4-0.6,5-2.7,9.4-3.7C198.7,164.9,207.1,165.1,210.7,166.1z"/></g></g></g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/images/rtimelogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

110
assets/images/rtimelogo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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

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

@ -0,0 +1,215 @@
{
"@@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",
"customColorSetting": "Button Color",
"selectButtonColor": "Select Button Primary Color",
"pickColor": "Pick a color",
"select": "Select",
"resetColor": "Reset Color"
}

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

@ -0,0 +1,215 @@
{
"@@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",
"customColorSetting": "Couleur des Boutons",
"selectButtonColor": "Sélectionner la couleur principale des boutons",
"pickColor": "Choisir une couleur",
"select": "Sélectionner",
"resetColor": "Réinitialiser la couleur"
}

View File

@ -0,0 +1,777 @@
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;
/// No description provided for @customColorSetting.
///
/// In en, this message translates to:
/// **'Button Color'**
String get customColorSetting;
/// No description provided for @selectButtonColor.
///
/// In en, this message translates to:
/// **'Select Button Primary Color'**
String get selectButtonColor;
/// No description provided for @pickColor.
///
/// In en, this message translates to:
/// **'Pick a color'**
String get pickColor;
/// No description provided for @select.
///
/// In en, this message translates to:
/// **'Select'**
String get select;
/// No description provided for @resetColor.
///
/// In en, this message translates to:
/// **'Reset Color'**
String get resetColor;
}
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,360 @@
// 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';
@override
String get customColorSetting => 'Button Color';
@override
String get selectButtonColor => 'Select Button Primary Color';
@override
String get pickColor => 'Pick a color';
@override
String get select => 'Select';
@override
String get resetColor => 'Reset Color';
}

View File

@ -0,0 +1,360 @@
// 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';
@override
String get customColorSetting => 'Couleur des Boutons';
@override
String get selectButtonColor => 'Sélectionner la couleur principale des boutons';
@override
String get pickColor => 'Choisir une couleur';
@override
String get select => 'Sélectionner';
@override
String get resetColor => 'Réinitialiser la couleur';
}

View File

@ -1,38 +1,260 @@
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';
import 'package:rtime/providers/color_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(),
),
ChangeNotifierProvider(
create: (context) => ColorProvider(),
),
],
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 Consumer3<LocaleProvider, ThemeProvider, ColorProvider>(
builder: (context, localeProvider, themeProvider, colorProvider, child) {
final customAccentColor = colorProvider.accentColor;
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.yellow[700],
colorScheme: ColorScheme.light(
primary: Colors.yellow[700]!,
secondary: Colors.teal.shade400,
surface: Colors.white,
background: Colors.white,
error: Colors.red.shade700,
onPrimary: Colors.black87,
onSecondary: Colors.white,
onSurface: Colors.black87,
onBackground: Colors.black87,
),
scaffoldBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
backgroundColor: Colors.yellow[700],
foregroundColor: Colors.black87,
elevation: 0.5,
iconTheme: const IconThemeData(color: Colors.black87),
titleTextStyle: const TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: customAccentColor,
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: customAccentColor.withOpacity(0.1),
),
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,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: customAccentColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: customAccentColor,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
side: BorderSide(color: customAccentColor),
foregroundColor: customAccentColor,
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: customAccentColor, 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.blueGrey[600]!,
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: customAccentColor,
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: customAccentColor.withOpacity(0.3),
),
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,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: customAccentColor,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: customAccentColor,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
side: BorderSide(color: customAccentColor),
foregroundColor: customAccentColor,
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: customAccentColor, 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,395 @@
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: Theme.of(context).appBarTheme.foregroundColor,
iconTheme: Theme.of(context).appBarTheme.iconTheme,
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: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Theme.of(context).colorScheme.primary, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 80,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
)
: null,
),
),
if (_isEditing) ...[
const SizedBox(height: 15),
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 10.0,
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,
),
),
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,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: _isEditing ? Colors.blueGrey[600]! : Colors.white30),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: _isEditing ? Colors.blueGrey[600]! : Colors.white30),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: _isEditing ? Colors.blueGrey[600]! : Colors.white30),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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: Theme.of(context).elevatedButtonTheme.style?.copyWith(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 40, vertical: 15)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
)),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
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,
),
const SizedBox(height: 15),
_isLoadingFlights
? const Center(child: CircularProgressIndicator())
: _associatedFlights.isEmpty
? Center(
child: Text(
l10n.noFlightsYet,
style: Theme.of(context).textTheme.titleMedium,
),
)
: 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: Icon(Icons.arrow_forward_ios,
size: 20, color: Theme.of(context).colorScheme.onSurface),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlightDetailPage(flight: flight),
),
).then((_) => _loadAssociatedFlights());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,349 @@
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: Theme.of(context).appBarTheme.foregroundColor,
iconTheme: Theme.of(context).appBarTheme.iconTheme,
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: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Theme.of(context).colorScheme.primary, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 80,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
)
: null,
),
),
if (_isEditing) ...[
const SizedBox(height: 15),
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 10.0,
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,
),
),
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,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: _isEditing ? Colors.blueGrey[600]! : Colors.white30),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterDroneName;
}
return null;
},
),
const SizedBox(height: 30),
if (_isEditing)
Center(
child: ElevatedButton(
onPressed: _saveDrone,
style: Theme.of(context).elevatedButtonTheme.style?.copyWith(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 40, vertical: 15)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
)),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
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,
),
const SizedBox(height: 15),
_isLoadingFlights
? const Center(child: CircularProgressIndicator())
: _associatedFlights.isEmpty
? Center(
child: Text(
l10n.noFlightsYet,
style: Theme.of(context).textTheme.titleMedium,
),
)
: 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: Icon(Icons.arrow_forward_ios,
size: 20, color: Theme.of(context).colorScheme.onSurface),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlightDetailPage(flight: flight),
),
).then((_) => _loadAssociatedFlights());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,358 @@
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';
import 'package:provider/provider.dart';
import 'package:rtime/providers/color_provider.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 colorProvider = Provider.of<ColorProvider>(context);
final accentColor = colorProvider.accentColor;
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).appBarTheme.backgroundColor,
foregroundColor: Theme.of(context).appBarTheme.foregroundColor,
iconTheme: Theme.of(context).iconTheme,
actions: [
IconButton(
icon: Icon(Icons.delete_forever, color: Theme.of(context).colorScheme.error),
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.headlineSmall!.copyWith(
color: Theme.of(context).textTheme.headlineSmall!.color,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Divider(color: Theme.of(context).dividerColor),
_buildImageAndDetailRow(
context,
l10n.drone,
_associatedDrone?.name ?? l10n.unknown,
_droneDisplayImage,
Icons.flight_takeoff,
accentColor,
),
_buildImageAndDetailRow(
context,
l10n.battery,
_associatedBattery?.name ?? l10n.unknown,
_batteryDisplayImage,
Icons.battery_std,
accentColor,
),
_buildDetailRow(
l10n.startTime,
'${startDate.toLocal().toString().split(' ')[0]} ${startDate.toLocal().toString().split(' ')[1].substring(0, 5)}',
accentColor),
_buildDetailRow(
l10n.endTime,
'${endDate.toLocal().toString().split(' ')[0]} ${endDate.toLocal().toString().split(' ')[1].substring(0, 5)}',
accentColor),
_buildDetailRow(l10n.duration,
_formatDuration(flight.startTimestamp, flight.endTimestamp),
accentColor),
],
),
),
),
if (flight.locationLat != null && flight.locationLong != null) ...[
Text(
l10n.flightLocation,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
Container(
height: 300,
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
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: Icon(
Icons.location_on,
color: accentColor,
size: 40.0,
),
),
],
),
],
),
),
),
] else ...[
const SizedBox(height: 20),
Center(
child: Text(
l10n.noLocationData,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildDetailRow(String label, String value, Color labelColor) {
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: labelColor,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
),
],
),
);
}
Widget _buildImageAndDetailRow(
BuildContext context, String label, String value, Image? displayImage, IconData defaultIcon, Color labelColor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 2,
child: Text(
'$label:',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: labelColor,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
flex: 3,
child: Row(
children: [
Expanded(
child: Text(
value,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
),
Container(
width: 40,
height: 40,
margin: const EdgeInsets.only(left: 12),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
image: displayImage != null
? DecorationImage(
image: displayImage.image,
fit: BoxFit.cover,
)
: null,
),
child: displayImage == null
? Icon(
defaultIcon,
size: 24,
color: Theme.of(context).iconTheme.color,
)
: null,
),
],
),
),
],
),
);
}
}

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

@ -0,0 +1,385 @@
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:provider/provider.dart';
import 'package:rtime/providers/color_provider.dart';
import 'package:rtime/widgets/page_transition_animations.dart';
import 'package:flutter_svg/flutter_svg.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)!;
final customAccentColor = Provider.of<ColorProvider>(context).accentColor;
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: [
SvgPicture.asset(
'assets/images/rtimelogo.svg',
height: 48,
width: 48,
colorFilter: ColorFilter.mode(customAccentColor, BlendMode.srcIn),
),
const SizedBox(width: 8),
Text(
l10n.appTitle,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: customAccentColor,
fontFamily: 'Montserrat',
),
),
const Spacer(),
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!.copyWith(color: Colors.white)),
],
),
),
),
);
} 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!.copyWith(color: Colors.white)),
],
),
),
),
);
}
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!.copyWith(color: Colors.white)),
],
),
),
),
);
} 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!.copyWith(color: Colors.white)),
],
),
),
),
);
}
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: Theme.of(context).colorScheme.onBackground),
),
);
} 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: Icon(Icons.flight_takeoff,
color: customAccentColor, // Use the user's accent color here
size: 32),
title: Text(
'${flight.name} - ${DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000).toLocal().toString().split(' ')[0]}',
style: Theme.of(context).textTheme.titleMedium,
),
trailing: Icon(Icons.arrow_forward_ios,
size: 20, color: Theme.of(context).colorScheme.onSurface),
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, color: Colors.white),
),
icon: const Icon(Icons.add_to_photos, size: 28, color: Colors.white),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}

View File

@ -0,0 +1,229 @@
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 uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
final loadedImage = await ImagesManager.instance.loadImage(uuid);
setState(() {
_selectedImageUuid = uuid;
_displayImage = loadedImage;
});
}
}
void _saveBattery() async {
if (_formKey.currentState!.validate()) {
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) {}
}
}
@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: [
GestureDetector(
onTap: () => _pickImage(ImageSource.camera),
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.blueGrey[600]!, 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),
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 10.0,
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,
),
),
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: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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: Theme.of(context).elevatedButtonTheme.style?.copyWith(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 40, vertical: 15)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
)),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
child: Text(
l10n.saveBattery,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,177 @@
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 uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
final loadedImage = await ImagesManager.instance.loadImage(uuid);
setState(() {
_selectedImageUuid = uuid;
_displayImage = loadedImage;
});
}
}
void _saveDrone() async {
if (_formKey.currentState!.validate()) {
final newDrone = Drone(
name: _nameController.text.trim(),
imageUuid: _selectedImageUuid,
);
try {
await DbHelper.instance.insertDrone(newDrone);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {}
}
}
@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: [
GestureDetector(
onTap: () => _pickImage(ImageSource.camera),
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.blueGrey[600]!, 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),
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 10.0,
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,
),
),
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: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueGrey[600]!),
),
),
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: Theme.of(context).elevatedButtonTheme.style?.copyWith(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 40, vertical: 15)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
)),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
child: Text(
l10n.saveDrone,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,539 @@
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';
import 'package:provider/provider.dart';
import 'package:rtime/providers/color_provider.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';
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';
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');
if (duration.inHours > 0) {
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
} else {
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colorProvider = Provider.of<ColorProvider>(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, style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white)),
);
}).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)', style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white)),
);
}).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: colorProvider.accentColor),
),
),
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: Icon(
Icons.location_on,
color: colorProvider.accentColor,
size: 40,
),
),
],
),
],
),
),
),
Center(
child: Text(
formattedTime,
style: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
color: colorProvider.accentColor,
fontFamily: 'RobotoMono',
),
),
),
const SizedBox(height: 40),
Center(
child: isFlightActive
? ElevatedButton.icon(
onPressed: _stopFlight,
icon: const Icon(Icons.stop, size: 30, color: Colors.white),
label: Text(
l10n.stopFlight,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
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: [
CircularProgressIndicator(color: colorProvider.accentColor),
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, color: Colors.white),
label: Text(
l10n.startFlight,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: colorProvider.accentColor,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
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,204 @@
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';
import 'package:rtime/providers/color_provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
late Color _pickerColor;
@override
void initState() {
super.initState();
_pickerColor = Provider.of<ColorProvider>(context, listen: false).accentColor;
}
void _onColorChanged(Color color) {
setState(() => _pickerColor = color);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final currentLocale = Localizations.localeOf(context);
final themeProvider = Provider.of<ThemeProvider>(context);
final colorProvider = Provider.of<ColorProvider>(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: SingleChildScrollView(
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('English', style: Theme.of(context).textTheme.titleMedium),
trailing: currentLocale.languageCode == 'en'
? Icon(Icons.check_circle, color: colorProvider.accentColor)
: null,
onTap: () {
final localeProvider = Provider.of<LocaleProvider>(context, listen: false);
localeProvider.setLocale(const Locale('en', ''));
},
),
ListTile(
title: Text('Français', style: Theme.of(context).textTheme.titleMedium),
trailing: currentLocale.languageCode == 'fr'
? Icon(Icons.check_circle, color: colorProvider.accentColor)
: 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: colorProvider.accentColor)
: 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: colorProvider.accentColor)
: 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: colorProvider.accentColor)
: null,
onTap: () {
themeProvider.setThemeMode(ThemeMode.dark);
},
),
],
),
),
const SizedBox(height: 30),
Text(
l10n.customColorSetting,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
Card(
color: Theme.of(context).cardTheme.color,
child: Column(
children: [
ListTile(
title: Text(
l10n.selectButtonColor,
style: Theme.of(context).textTheme.titleMedium,
),
trailing: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: colorProvider.accentColor,
shape: BoxShape.circle,
border: Border.all(color: Theme.of(context).dividerColor),
),
),
onTap: () {
setState(() {
_pickerColor = colorProvider.accentColor;
});
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(l10n.pickColor),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: _pickerColor,
onColorChanged: _onColorChanged,
pickerAreaHeightPercent: 0.8,
enableAlpha: false,
displayThumbColor: true,
paletteType: PaletteType.hsv,
),
),
actions: <Widget>[
TextButton(
child: Text(l10n.cancel),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text(l10n.select),
onPressed: () {
colorProvider.setAccentColor(_pickerColor);
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
ListTile(
title: Text(l10n.resetColor, style: Theme.of(context).textTheme.titleMedium),
trailing: const Icon(Icons.restore),
onTap: () {
colorProvider.setAccentColor(Colors.yellow[700]!);
setState(() {
_pickerColor = Colors.yellow[700]!;
});
},
),
],
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,31 @@
// providers/color_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ColorProvider extends ChangeNotifier {
Color _accentColor = Colors.blue.shade600;
Color get accentColor => _accentColor;
ColorProvider() {
_loadAccentColor();
}
void _loadAccentColor() async {
final prefs = await SharedPreferences.getInstance();
final int? colorValue = prefs.getInt('customAccentColor');
if (colorValue != null) {
_accentColor = Color(colorValue);
}
notifyListeners();
}
void setAccentColor(Color color) async {
if (_accentColor != color) {
_accentColor = color;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('customAccentColor', color.value);
}
}
}

View File

@ -0,0 +1,50 @@
// providers/local_provider.dart
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,29 @@
// providers/theme_provider.dart
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);
}
}
}

83
lib/removecom.py Normal file
View File

@ -0,0 +1,83 @@
import os
import sys
import re
def clean_comments_in_file(filepath):
"""
Supprime le reste de la ligne à partir de '//' dans un fichier donné,
en essayant d'éviter les URL. Gère aussi les commentaires /* ... */.
"""
try:
with open(filepath, 'r', encoding='utf-8') as f_read:
content = f_read.read()
# Première passe : supprimer les commentaires /* ... */
# Utilise re.DOTALL pour que . corresponde aussi aux retours à la ligne
# et non-greedy quantifier *?
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
cleaned_lines = []
for line in content.splitlines():
# Vérifie si la ligne contient une URL avant de chercher un commentaire
if re.search(r'https?://', line):
# Si une URL est trouvée, nous sommes très prudents.
# Nous ne supprimons les commentaires que s'ils sont CLAIREMENT après une URL et non mélangés
# Cela reste une heuristique et n'est pas parfait.
comment_index = line.find('//')
if comment_index != -1 and not re.search(r'["\']https?://.*?//', line): # Évite les // dans les URLs entre guillemets
# On tente de voir si le commentaire est vraiment à la fin de la ligne,
# après d'éventuelles guillemets.
# Ceci est une simplification et peut encore échouer.
if comment_index > line.rfind('"') and comment_index > line.rfind("'"):
cleaned_lines.append(line[:comment_index].rstrip())
else:
cleaned_lines.append(line) # Conserve la ligne si le // est potentiellement dans une chaîne/URL
else:
cleaned_lines.append(line)
else:
# Si pas d'URL, on peut être plus agressif avec les //
if '//' in line:
index = line.find('//')
cleaned_lines.append(line[:index].rstrip())
else:
cleaned_lines.append(line)
# Rejoindre les lignes avec des retours à la ligne.
# Ajoute un retour à la ligne final si le fichier en avait un.
final_content = '\n'.join(cleaned_lines)
if content.endswith('\n') and not final_content.endswith('\n'):
final_content += '\n'
with open(filepath, 'w', encoding='utf-8') as f_write:
f_write.write(final_content)
print(f"Commentaires nettoyés dans : {filepath}")
except Exception as e:
print(f"Erreur lors du traitement de {filepath} : {e}")
def main():
if len(sys.argv) < 2:
print("Usage: python clean_comments.py <path_to_directory>")
print("Exemple: python clean_comments.py ./mon_projet_flutter")
sys.exit(1)
target_directory = sys.argv[1]
if not os.path.isdir(target_directory):
print(f"Erreur : Le chemin '{target_directory}' n'est pas un répertoire valide.")
sys.exit(1)
print(f"Nettoyage des commentaires '//' et '/* ... */' dans le répertoire : {target_directory}")
print("-" * 70)
for root, _, files in os.walk(target_directory):
for file in files:
filepath = os.path.join(root, file)
# Filtrez les types de fichiers que vous souhaitez traiter
if filepath.endswith(('.dart', '.js', '.ts', '.java', '.cpp', '.c', '.h', '.py')):
clean_comments_in_file(filepath)
print("-" * 70)
print("Nettoyage terminé.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/images_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:rtime/providers/color_provider.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 colorProvider = Provider.of<ColorProvider>(context);
final accentColor = colorProvider.accentColor;
final ShapeBorder? cardShape = Theme.of(context).cardTheme.shape;
final BorderRadius cardBorderRadius = (cardShape is RoundedRectangleBorder)
? (cardShape).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: [
SizedBox(
width: 80,
height: 80,
child: battery.imageUuid != null && battery.imageUuid!.isNotEmpty
? FutureBuilder<Image?>(
future: ImagesManager.instance.loadImage(battery.imageUuid!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: accentColor));
} else if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
return Icon(
Icons.battery_charging_full,
size: 80,
color: accentColor,
);
} else {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: snapshot.data!,
);
}
},
)
: Icon(
Icons.battery_charging_full,
size: 80,
color: accentColor,
),
),
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,89 @@
import 'package:flutter/material.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/images_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:rtime/providers/color_provider.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 colorProvider = Provider.of<ColorProvider>(context);
final accentColor = colorProvider.accentColor;
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: [
SizedBox(
width: 80,
height: 80,
child: drone.imageUuid != null && drone.imageUuid!.isNotEmpty
? FutureBuilder<Image?>(
future: ImagesManager.instance.loadImage(drone.imageUuid!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: accentColor));
} else if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
return SvgPicture.asset(
'assets/images/drone.svg',
width: 50,
height: 50,
colorFilter: ColorFilter.mode(accentColor, BlendMode.srcIn),
);
} else {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: snapshot.data!,
);
}
},
)
: SvgPicture.asset(
'assets/images/drone.svg',
width: 50,
height: 50,
colorFilter: ColorFilter.mode(accentColor, BlendMode.srcIn),
),
),
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

@ -129,6 +129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -185,6 +193,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:
@ -262,6 +278,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_launcher_icons:
dependency: "direct main"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints:
dependency: "direct dev"
description:
@ -270,6 +302,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:
@ -278,6 +323,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -296,6 +349,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 +533,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 +573,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 +613,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 +661,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 +677,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:
@ -552,6 +701,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
@ -624,6 +781,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 +805,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 +837,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 +1082,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:
@ -853,6 +1098,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331"
url: "https://pub.dev"
source: hosted
version: "1.1.17"
vector_math:
dependency: transitive
description:
@ -901,6 +1170,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

@ -30,7 +30,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_colorpicker: ^1.0.0
flutter_launcher_icons: ^0.13.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
@ -45,6 +46,15 @@ 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
flutter_svg: ^2.0.10+1
dev_dependencies:
flutter_test:
@ -62,16 +72,15 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# 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:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/images/ # This is the correct and only place to declare your assets folder.
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
@ -98,3 +107,11 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
# This section needs to be at the root level, not under "flutter:"
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/images/rtimelogo.png"
min_sdk_android: 21
remove_alpha_ios: true

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
)