Link between front-end and back-end

This commit is contained in:
2025-07-09 12:41:00 +02:00
parent 6735ddb5fd
commit 1b261c08bb
31 changed files with 5507 additions and 40 deletions

View File

@ -0,0 +1,425 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/images_manager.dart';
import 'package:image_picker/image_picker.dart';
import 'package:rtime/models/flight.dart';
import 'package:rtime/pages/flight_detail_page.dart';
class BatteryDetailPage extends StatefulWidget {
final Battery battery;
const BatteryDetailPage({super.key, required this.battery});
@override
State<BatteryDetailPage> createState() => _BatteryDetailPageState();
}
class _BatteryDetailPageState extends State<BatteryDetailPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _typeController;
late TextEditingController _voltageController;
String? _currentImageUuid;
Image? _displayImage;
bool _isEditing = false;
List<Flight> _associatedFlights = [];
bool _isLoadingFlights = true;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.battery.name);
_typeController = TextEditingController(text: widget.battery.type);
_voltageController = TextEditingController(text: widget.battery.voltage.toString());
_currentImageUuid = widget.battery.imageUuid;
_loadImage();
_loadAssociatedFlights();
}
@override
void dispose() {
_nameController.dispose();
_typeController.dispose();
_voltageController.dispose();
super.dispose();
}
Future<void> _loadImage() async {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!);
setState(() {
_displayImage = loadedImage;
});
} else {
setState(() {
_displayImage = null;
});
}
}
Future<void> _loadAssociatedFlights() async {
setState(() {
_isLoadingFlights = true;
});
try {
final allFlights = await DbHelper.instance.getBatteryFlights(widget.battery.id!);
_associatedFlights = allFlights
.where((flight) => flight.batteryId == widget.battery.id)
.toList();
_associatedFlights.sort((a, b) => b.startTimestamp.compareTo(a.startTimestamp));
setState(() {
_isLoadingFlights = false;
});
} catch (e) {
if (mounted) {
setState(() {
_isLoadingFlights = false;
});
}
}
}
Future<void> _pickImage(ImageSource source) async {
final uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
if (_currentImageUuid != null && _currentImageUuid != uuid) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
}
setState(() {
_currentImageUuid = uuid;
});
await _loadImage();
}
}
Future<void> _deleteImage() async {
if (_currentImageUuid != null) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
setState(() {
_currentImageUuid = null;
_displayImage = null;
});
}
}
Future<void> _saveBattery() async {
if (_formKey.currentState!.validate()) {
final l10n = AppLocalizations.of(context)!;
final updatedBattery = Battery(
id: widget.battery.id,
name: _nameController.text.trim(),
type: _typeController.text.trim(),
voltage: double.parse(_voltageController.text.trim()),
imageUuid: _currentImageUuid,
);
try {
await DbHelper.instance.updateBattery(updatedBattery);
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
}
}
}
}
Future<void> _deleteBattery() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.deleteBatteryConfirmationTitle),
content: Text(l10n.deleteBatteryConfirmationMessage(widget.battery.name)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete),
),
],
),
);
if (confirm == true) {
try {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
}
await DbHelper.instance.deleteBattery(widget.battery.id!);
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
}
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(_isEditing ? l10n.editBattery : l10n.batteryDetails),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
actions: [
IconButton(
icon: Icon(_isEditing ? Icons.save : Icons.edit),
onPressed: () {
if (_isEditing) {
_saveBattery();
}
setState(() {
_isEditing = !_isEditing;
});
},
),
if (!_isEditing)
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.redAccent),
onPressed: _deleteBattery,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Center(
child: Container(
width: 180,
height: 180,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.tealAccent, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 80,
color: Colors.blueGrey[400],
)
: null,
),
),
if (_isEditing) ...[
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: Text(l10n.takePhoto),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: Text(l10n.chooseFromGallery),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
],
),
if (_currentImageUuid != null)
TextButton.icon(
onPressed: _deleteImage,
icon: const Icon(Icons.clear, color: Colors.redAccent),
label: Text(l10n.removeImage, style: const TextStyle(color: Colors.redAccent)),
),
],
const SizedBox(height: 30),
TextFormField(
controller: _nameController,
readOnly: !_isEditing,
decoration: InputDecoration(
labelText: l10n.batteryName,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.tealAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryName;
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _typeController,
readOnly: !_isEditing,
decoration: InputDecoration(
labelText: l10n.batteryType,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.tealAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryType;
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _voltageController,
keyboardType: TextInputType.number,
readOnly: !_isEditing,
decoration: InputDecoration(
labelText: l10n.batteryVoltage,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.tealAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryVoltage;
}
if (double.tryParse(value) == null) {
return l10n.pleaseEnterValidNumber;
}
return null;
},
),
const SizedBox(height: 30),
if (_isEditing)
Center(
child: ElevatedButton(
onPressed: _saveBattery,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
l10n.saveChanges,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 30),
Text(
l10n.latestFlights,
style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white),
),
const SizedBox(height: 15),
_isLoadingFlights
? const Center(child: CircularProgressIndicator())
: _associatedFlights.isEmpty
? Center(
child: Text(
l10n.noFlightsYet,
style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _associatedFlights.length,
itemBuilder: (context, index) {
final flight = _associatedFlights[index];
final DateTime flightDate =
DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000);
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
leading: const Icon(Icons.flight_takeoff,
color: Colors.orangeAccent, size: 32),
title: Text(
'${flight.name} - ${flightDate.toLocal().toString().split(' ')[0]}',
style: Theme.of(context).textTheme.titleMedium,
),
trailing: const Icon(Icons.arrow_forward_ios,
size: 20, color: Colors.white54),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlightDetailPage(flight: flight),
),
).then((_) => _loadAssociatedFlights());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,367 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/images_manager.dart';
import 'package:image_picker/image_picker.dart';
import 'package:rtime/models/flight.dart';
import 'package:rtime/pages/flight_detail_page.dart';
class DroneDetailPage extends StatefulWidget {
final Drone drone;
const DroneDetailPage({super.key, required this.drone});
@override
State<DroneDetailPage> createState() => _DroneDetailPageState();
}
class _DroneDetailPageState extends State<DroneDetailPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
String? _currentImageUuid;
Image? _displayImage;
bool _isEditing = false;
List<Flight> _associatedFlights = [];
bool _isLoadingFlights = true;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.drone.name);
_currentImageUuid = widget.drone.imageUuid;
_loadImage();
_loadAssociatedFlights();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _loadImage() async {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
final loadedImage = await ImagesManager.instance.loadImage(_currentImageUuid!);
setState(() {
_displayImage = loadedImage;
});
} else {
setState(() {
_displayImage = null;
});
}
}
Future<void> _loadAssociatedFlights() async {
setState(() {
_isLoadingFlights = true;
});
try {
final allFlights = await DbHelper.instance.getDroneFlights(widget.drone.id!);
_associatedFlights = allFlights
.where((flight) => flight.droneId == widget.drone.id)
.toList();
_associatedFlights.sort((a, b) => b.startTimestamp.compareTo(a.startTimestamp));
setState(() {
_isLoadingFlights = false;
});
} catch (e) {
if (mounted) {
setState(() {
_isLoadingFlights = false;
});
}
}
}
Future<void> _pickImage(ImageSource source) async {
final uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
if (_currentImageUuid != null && _currentImageUuid != uuid) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
}
setState(() {
_currentImageUuid = uuid;
});
await _loadImage();
}
}
Future<void> _deleteImage() async {
if (_currentImageUuid != null) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
setState(() {
_currentImageUuid = null;
_displayImage = null;
});
}
}
Future<void> _saveDrone() async {
if (_formKey.currentState!.validate()) {
final l10n = AppLocalizations.of(context)!;
final updatedDrone = Drone(
id: widget.drone.id,
name: _nameController.text.trim(),
imageUuid: _currentImageUuid,
);
try {
await DbHelper.instance.updateDrone(updatedDrone);
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
}
}
}
}
Future<void> _deleteDrone() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.deleteDroneConfirmationTitle),
content: Text(l10n.deleteDroneConfirmationMessage(widget.drone.name)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete),
),
],
),
);
if (confirm == true) {
try {
if (_currentImageUuid != null && _currentImageUuid!.isNotEmpty) {
await ImagesManager.instance.deleteImage(_currentImageUuid!);
}
await DbHelper.instance.deleteDrone(widget.drone.id!);
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
}
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(_isEditing ? l10n.editDrone : l10n.droneDetails),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
actions: [
IconButton(
icon: Icon(_isEditing ? Icons.save : Icons.edit),
onPressed: () {
if (_isEditing) {
_saveDrone();
}
setState(() {
_isEditing = !_isEditing;
});
},
),
if (!_isEditing)
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.redAccent),
onPressed: _deleteDrone,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Center(
child: Container(
width: 180,
height: 180,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.lightBlueAccent, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 80,
color: Colors.blueGrey[400],
)
: null,
),
),
if (_isEditing) ...[
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: Text(l10n.takePhoto),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: Text(l10n.chooseFromGallery),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
],
),
if (_currentImageUuid != null)
TextButton.icon(
onPressed: _deleteImage,
icon: const Icon(Icons.clear, color: Colors.redAccent),
label: Text(l10n.removeImage, style: const TextStyle(color: Colors.redAccent)),
),
],
const SizedBox(height: 30),
TextFormField(
controller: _nameController,
readOnly: !_isEditing,
decoration: InputDecoration(
labelText: l10n.droneName,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterDroneName;
}
return null;
},
),
const SizedBox(height: 30),
if (_isEditing)
Center(
child: ElevatedButton(
onPressed: _saveDrone,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
l10n.saveChanges,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
const SizedBox(height: 30),
Text(
l10n.latestFlights,
style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white),
),
const SizedBox(height: 15),
_isLoadingFlights
? const Center(child: CircularProgressIndicator())
: _associatedFlights.isEmpty
? Center(
child: Text(
l10n.noFlightsYet,
style: Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white70),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _associatedFlights.length,
itemBuilder: (context, index) {
final flight = _associatedFlights[index];
final DateTime flightDate =
DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000);
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
leading: const Icon(Icons.flight_takeoff,
color: Colors.orangeAccent, size: 32),
title: Text(
'${flight.name} - ${flightDate.toLocal().toString().split(' ')[0]}',
style: Theme.of(context).textTheme.titleMedium,
),
trailing: const Icon(Icons.arrow_forward_ios,
size: 20, color: Colors.white54),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlightDetailPage(flight: flight),
),
).then((_) => _loadAssociatedFlights());
},
),
);
},
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,373 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/flight.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:rtime/images_manager.dart';
class FlightDetailPage extends StatefulWidget {
final Flight flight;
const FlightDetailPage({super.key, required this.flight});
@override
State<FlightDetailPage> createState() => _FlightDetailPageState();
}
class _FlightDetailPageState extends State<FlightDetailPage> {
Drone? _associatedDrone;
Battery? _associatedBattery;
Image? _droneDisplayImage;
Image? _batteryDisplayImage;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadAssociatedData();
}
Future<void> _loadImage(String? uuid, Function(Image?) setImage) async {
if (uuid != null && uuid.isNotEmpty) {
final loadedImage = await ImagesManager.instance.loadImage(uuid);
if (mounted) {
setImage(loadedImage);
}
} else {
if (mounted) {
setImage(null);
}
}
}
Future<void> _loadAssociatedData() async {
try {
final droneList = await DbHelper.instance.getDrones();
final batteryList = await DbHelper.instance.getBatteries();
final drone = droneList.firstWhere(
(d) => d.id == widget.flight.droneId,
orElse: () => Drone(name: 'Unknown Drone'));
final battery = batteryList.firstWhere(
(b) => b.id == widget.flight.batteryId,
orElse: () => Battery(
name: 'Unknown Battery', type: '', voltage: 0.0));
await _loadImage(drone.imageUuid, (image) {
setState(() {
_droneDisplayImage = image;
});
});
await _loadImage(battery.imageUuid, (image) {
setState(() {
_batteryDisplayImage = image;
});
});
setState(() {
_associatedDrone = drone;
_associatedBattery = battery;
_isLoading = false;
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
String _formatDuration(int startTimestamp, int endTimestamp) {
final duration = Duration(seconds: endTimestamp - startTimestamp);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
Future<void> _deleteFlight() async {
final l10n = AppLocalizations.of(context)!;
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.deleteFlightConfirmationTitle),
content: Text(l10n.deleteFlightConfirmationMessage(widget.flight.name)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(l10n.delete),
),
],
),
);
if (confirm == true) {
try {
await DbHelper.instance.deleteFlight(widget.flight.id!);
if (mounted) {
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
}
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final flight = widget.flight;
final DateTime startDate =
DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000);
final DateTime endDate =
DateTime.fromMillisecondsSinceEpoch(flight.endTimestamp * 1000);
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(l10n.flightDetails),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.redAccent),
onPressed: _deleteFlight,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
flight.name,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Divider(color: Colors.grey[300]),
_buildImageAndDetailRow(
context,
l10n.drone,
_associatedDrone?.name ?? l10n.unknown,
_droneDisplayImage,
Icons.flight_takeoff,
),
_buildImageAndDetailRow(
context,
l10n.battery,
_associatedBattery?.name ?? l10n.unknown,
_batteryDisplayImage,
Icons.battery_std,
),
_buildDetailRow(
l10n.startTime,
'${startDate.toLocal().toString().split(' ')[0]} ${startDate.toLocal().toString().split(' ')[1].substring(0, 5)}'),
_buildDetailRow(
l10n.endTime,
'${endDate.toLocal().toString().split(' ')[0]} ${endDate.toLocal().toString().split(' ')[1].substring(0, 5)}'),
_buildDetailRow(
l10n.duration,
_formatDuration(
flight.startTimestamp, flight.endTimestamp)),
],
),
),
),
if (flight.locationLat != null && flight.locationLong != null) ...[
Text(
l10n.flightLocation,
style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white),
),
const SizedBox(height: 15),
Container(
height: 300,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(15),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: FlutterMap(
options: MapOptions(
initialCenter: LatLng(
flight.locationLat!, flight.locationLong!),
initialZoom: 15.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
TileLayer(
urlTemplate:
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
userAgentPackageName: 'com.example.rtime',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(
flight.locationLat!, flight.locationLong!),
width: 80,
height: 80,
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 40.0,
),
),
],
),
],
),
),
),
] else ...[
const SizedBox(height: 20),
Center(
child: Text(
l10n.noLocationData,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Colors.white70),
),
),
],
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
'$label:',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
);
}
Widget _buildImageAndDetailRow(
BuildContext context, String label, String value, Image? displayImage, IconData defaultIcon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blueGrey[600]!, width: 1),
image: displayImage != null
? DecorationImage(
image: displayImage.image,
fit: BoxFit.cover,
)
: null,
),
child: displayImage == null
? Icon(
defaultIcon,
size: 24,
color: Colors.blueGrey[400],
)
: null,
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
'$label:',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
),
],
),
);
}
}

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

@ -0,0 +1,389 @@
import 'package:flutter/material.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/models/flight.dart';
import 'package:rtime/widgets/drone_cart.dart';
import 'package:rtime/widgets/battery_card.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/pages/settings_page.dart';
import 'package:rtime/pages/new_flight_page.dart';
import 'package:rtime/pages/new_drone_page.dart';
import 'package:rtime/pages/new_battery_page.dart';
import 'package:rtime/pages/drone_detail_page.dart';
import 'package:rtime/pages/battery_detail_page.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/pages/flight_detail_page.dart';
import 'package:rtime/widgets/page_transition_animations.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<List<Drone>> _dronesFuture;
late Future<List<Battery>> _batteriesFuture;
late Future<List<Flight>> _flightsFuture;
@override
void initState() {
super.initState();
_loadData();
}
void _loadData() {
setState(() {
_dronesFuture = DbHelper.instance.getDrones();
_batteriesFuture = DbHelper.instance.getBatteries();
_flightsFuture = DbHelper.instance.getFlights();
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: RefreshIndicator(
onRefresh: () async {
_loadData();
await Future.wait([_dronesFuture, _batteriesFuture, _flightsFuture]);
},
child: SingleChildScrollView(
padding: const EdgeInsets.only(
top: 60.0, left: 16.0, right: 16.0, bottom: 90.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.appTitle,
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.lightBlueAccent,
fontFamily: 'Montserrat',
),
),
IconButton(
icon:
const Icon(Icons.settings, size: 30, color: Colors.white70),
onPressed: () {
Navigator.of(context).push(
SlideRightPageRoute(
page: const SettingsPage(),
),
);
},
),
],
),
const SizedBox(height: 30),
Text(
l10n.yourDrones,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
SizedBox(
height: 170,
child: FutureBuilder<List<Drone>>(
future: _dronesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(
child: Text(
'${l10n.errorLoadingDrones}: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Card(
margin: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.of(context).push(
SlideUpPageRoute(
page: const NewDronePage(),
),
).then((_) => _loadData());
},
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline,
size: 50, color: Colors.blueGrey[400]),
const SizedBox(height: 10),
Text(l10n.addDrone,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall),
],
),
),
),
);
} else {
final drones = snapshot.data!;
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: drones.length + 1,
itemBuilder: (context, index) {
if (index == drones.length) {
return Card(
margin: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.of(context).push(
SlideUpPageRoute(
page: const NewDronePage(),
),
).then((_) => _loadData());
},
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline,
size: 50, color: Colors.blueGrey[400]),
const SizedBox(height: 10),
Text(l10n.addDrone,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall),
],
),
),
),
);
}
return DroneCard(
drone: drones[index],
onTap: () {
Navigator.of(context).push(
SlideRightPageRoute(
page: DroneDetailPage(drone: drones[index]),
),
).then((result) {
if (result == true) {
_loadData();
}
});
},
);
},
);
}
},
),
),
const Divider(
height: 50, thickness: 2, indent: 0, endIndent: 0, color: Colors.white10),
Text(
l10n.yourBatteries,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
SizedBox(
height: 170,
child: FutureBuilder<List<Battery>>(
future: _batteriesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(
child: Text(
'${l10n.errorLoadingBatteries}: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Card(
margin: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.of(context).push(
SlideUpPageRoute(
page: const NewBatteryPage(),
),
).then((_) => _loadData());
},
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline,
size: 50, color: Colors.blueGrey[400]),
const SizedBox(height: 10),
Text(l10n.addBattery,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall),
],
),
),
),
);
} else {
final batteries = snapshot.data!;
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: batteries.length + 1,
itemBuilder: (context, index) {
if (index == batteries.length) {
return Card(
margin: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.of(context).push(
SlideUpPageRoute(
page: const NewBatteryPage(),
),
).then((_) => _loadData());
},
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline,
size: 50, color: Colors.blueGrey[400]),
const SizedBox(height: 10),
Text(l10n.addBattery,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall),
],
),
),
),
);
}
return BatteryCard(
battery: batteries[index],
onTap: () {
Navigator.of(context).push(
SlideRightPageRoute(
page: BatteryDetailPage(battery: batteries[index]),
),
).then((result) {
if (result == true) {
_loadData();
}
});
},
);
},
);
}
},
),
),
const Divider(
height: 50, thickness: 2, indent: 0, endIndent: 0, color: Colors.white10),
Text(
l10n.latestFlights,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
FutureBuilder<List<Flight>>(
future: _flightsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(
child: Text(
'${l10n.errorLoadingFlights}: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Text(
l10n.noFlightsYet,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Colors.white70),
),
);
} else {
final flights = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: flights.length,
itemBuilder: (context, index) {
final flight = flights[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
leading: const Icon(Icons.flight_takeoff,
color: Colors.orangeAccent, size: 32),
title: Text(
'${flight.name} - ${DateTime.fromMillisecondsSinceEpoch(flight.startTimestamp * 1000).toLocal().toString().split(' ')[0]}',
style: Theme.of(context).textTheme.titleMedium,
),
trailing: const Icon(Icons.arrow_forward_ios,
size: 20, color: Colors.white54),
onTap: () {
Navigator.of(context).push(
SlideRightPageRoute(
page: FlightDetailPage(flight: flight),
),
).then((_) => _loadData());
},
),
);
},
);
}
},
),
],
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).push(
SlideUpPageRoute(
page: const NewFlightPage(),
),
).then((_) => _loadData());
},
label: Text(
l10n.newFlight,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
icon: const Icon(Icons.add_to_photos, size: 28),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}

View File

@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/images_manager.dart';
import 'package:image_picker/image_picker.dart';
class NewBatteryPage extends StatefulWidget {
const NewBatteryPage({super.key});
@override
State<NewBatteryPage> createState() => _NewBatteryPageState();
}
class _NewBatteryPageState extends State<NewBatteryPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _typeController = TextEditingController();
final TextEditingController _voltageController = TextEditingController();
String? _selectedImageUuid;
Image? _displayImage;
@override
void dispose() {
_nameController.dispose();
_typeController.dispose();
_voltageController.dispose();
super.dispose();
}
Future<void> _pickImage(ImageSource source) async {
final l10n = AppLocalizations.of(context)!;
final uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
final loadedImage = await ImagesManager.instance.loadImage(uuid);
setState(() {
_selectedImageUuid = uuid;
_displayImage = loadedImage;
});
if (mounted) {
}
} else {
if (mounted) {
}
}
}
void _saveBattery() async {
if (_formKey.currentState!.validate()) {
final l10n = AppLocalizations.of(context)!;
final newBattery = Battery(
name: _nameController.text.trim(),
type: _typeController.text.trim(),
voltage: double.parse(_voltageController.text.trim()),
imageUuid: _selectedImageUuid,
);
try {
await DbHelper.instance.insertBattery(newBattery);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
if (mounted) {
}
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(l10n.addBattery),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Center(
child: Column(
children: [
Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.lightBlueAccent, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 60,
color: Colors.blueGrey[400],
)
: null,
),
const SizedBox(height: 10),
Text(l10n.batteryImage,
style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: Text(l10n.takePhoto),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: Text(l10n.chooseFromGallery),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
],
),
],
),
),
const SizedBox(height: 30),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n.batteryName,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryName;
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _typeController,
decoration: InputDecoration(
labelText: l10n.batteryType,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryType;
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
controller: _voltageController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: l10n.batteryVoltage,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterBatteryVoltage;
}
if (double.tryParse(value) == null) {
return l10n.pleaseEnterValidNumber;
}
return null;
},
),
const SizedBox(height: 30),
Center(
child: ElevatedButton(
onPressed: _saveBattery,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.black,
padding:
const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
l10n.saveBattery,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/images_manager.dart';
import 'package:image_picker/image_picker.dart';
class NewDronePage extends StatefulWidget {
const NewDronePage({super.key});
@override
State<NewDronePage> createState() => _NewDronePageState();
}
class _NewDronePageState extends State<NewDronePage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
String? _selectedImageUuid;
Image? _displayImage;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _pickImage(ImageSource source) async {
final l10n = AppLocalizations.of(context)!;
final uuid = await ImagesManager.instance.createImage(source);
if (uuid != null) {
final loadedImage = await ImagesManager.instance.loadImage(uuid);
setState(() {
_selectedImageUuid = uuid;
_displayImage = loadedImage;
});
if (mounted) {
}
} else {
if (mounted) {
}
}
}
void _saveDrone() async {
if (_formKey.currentState!.validate()) {
final l10n = AppLocalizations.of(context)!;
final newDrone = Drone(
name: _nameController.text.trim(),
imageUuid: _selectedImageUuid,
);
try {
await DbHelper.instance.insertDrone(newDrone);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
if (mounted) {
}
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(l10n.addDrone),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Center(
child: Column(
children: [
Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blueGrey[800],
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.lightBlueAccent, width: 2),
image: _displayImage != null
? DecorationImage(
image: _displayImage!.image,
fit: BoxFit.cover,
)
: null,
),
child: _displayImage == null
? Icon(
Icons.camera_alt,
size: 60,
color: Colors.blueGrey[400],
)
: null,
),
const SizedBox(height: 10),
Text(l10n.droneImage,
style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: Text(l10n.takePhoto),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: Text(l10n.chooseFromGallery),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[700],
foregroundColor: Colors.white,
),
),
],
),
],
),
),
const SizedBox(height: 30),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n.droneName,
labelStyle: const TextStyle(color: Colors.white70),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.pleaseEnterDroneName;
}
return null;
},
),
const SizedBox(height: 30),
Center(
child: ElevatedButton(
onPressed: _saveDrone,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.black,
padding:
const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
l10n.saveDrone,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,578 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/models/battery.dart';
import 'package:rtime/models/flight.dart';
import 'package:rtime/db/db_helper.dart';
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
class NewFlightPage extends StatefulWidget {
const NewFlightPage({super.key});
@override
State<NewFlightPage> createState() => _NewFlightPageState();
}
class _NewFlightPageState extends State<NewFlightPage> {
late Future<List<Drone>> _dronesFuture;
late Future<List<Battery>> _batteriesFuture;
Drone? selectedDrone;
Battery? selectedBattery;
bool isFlightActive = false;
Stopwatch stopwatch = Stopwatch();
Timer? timer;
String formattedTime = '00:00:00';
int? flightStartTime;
bool useGpsLocation = false;
Position? currentPosition;
bool isGettingLocation = false;
final MapController _mapController = MapController();
bool _initialMapCentered = false;
@override
void initState() {
super.initState();
_loadAvailableItems();
}
void _loadAvailableItems() {
_dronesFuture = DbHelper.instance.getDrones();
_batteriesFuture = DbHelper.instance.getBatteries();
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
Future<void> _getCurrentLocation() async {
if (!mounted) return;
setState(() {
isGettingLocation = true;
});
final l10n = AppLocalizations.of(context)!;
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (mounted) {
}
if (mounted) {
setState(() {
useGpsLocation = false;
});
}
if (mounted) {
setState(() {
isGettingLocation = false;
});
}
return;
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (mounted) {
}
if (mounted) {
setState(() {
useGpsLocation = false;
});
}
if (mounted) {
setState(() {
isGettingLocation = false;
});
}
return;
}
}
if (permission == LocationPermission.deniedForever) {
if (mounted) {
}
if (mounted) {
setState(() {
useGpsLocation = false;
});
}
if (mounted) {
setState(() {
isGettingLocation = false;
});
}
return;
}
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
timeLimit: const Duration(seconds: 15),
);
if (mounted) {
setState(() {
currentPosition = position;
if (_initialMapCentered && _mapController.camera.center != LatLng(position.latitude, position.longitude)) {
_mapController.move(
LatLng(currentPosition!.latitude, currentPosition!.longitude),
_mapController.camera.zoom,
);
}
});
}
if (mounted) {
}
} catch (e) {
if (mounted) {
}
if (mounted) {
setState(() {
useGpsLocation = false;
currentPosition = null;
});
}
} finally {
if (mounted) {
setState(() {
isGettingLocation = false;
});
}
}
}
void _startFlight() {
final l10n = AppLocalizations.of(context)!;
if (selectedDrone == null || selectedBattery == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.selectDroneBattery)),
);
return;
}
if (useGpsLocation) {
setState(() {
isGettingLocation = true;
});
_getCurrentLocation().then((_) {
if (useGpsLocation && currentPosition == null) {
return;
}
_proceedToStartFlight();
});
} else {
_proceedToStartFlight();
}
}
void _proceedToStartFlight() {
final l10n = AppLocalizations.of(context)!;
setState(() {
isFlightActive = true;
stopwatch.start();
flightStartTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
timer = Timer.periodic(const Duration(milliseconds: 100), (Timer t) {
if (stopwatch.isRunning) {
setState(() {
formattedTime = _formatDuration(stopwatch.elapsed);
});
}
});
});
}
Future<void> _stopFlight() async {
final l10n = AppLocalizations.of(context)!;
stopwatch.stop();
timer?.cancel();
final int flightEndTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final Duration flightDuration = stopwatch.elapsed;
final Flight newFlight = Flight(
name: '${selectedDrone!.name} - ${DateTime.fromMillisecondsSinceEpoch(flightStartTime! * 1000).toLocal().toString().split(' ')[0]}',
startTimestamp: flightStartTime!,
endTimestamp: flightEndTime,
droneId: selectedDrone!.id!,
batteryId: selectedBattery!.id!,
locationLat: currentPosition?.latitude,
locationLong: currentPosition?.longitude,
);
try {
await DbHelper.instance.insertFlight(newFlight);
if (mounted) {
}
} catch (e) {
if (mounted) {
}
} finally {
setState(() {
isFlightActive = false;
stopwatch.reset();
formattedTime = '00:00:00';
selectedDrone = null;
selectedBattery = null;
currentPosition = null;
useGpsLocation = false;
isGettingLocation = false;
_initialMapCentered = false;
});
if (mounted) {
Navigator.pop(context);
}
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(l10n.newFlight),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
l10n.chooseDrone,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
Card(
margin: const EdgeInsets.only(bottom: 20),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: FutureBuilder<List<Drone>>(
future: _dronesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('${l10n.errorLoadingDrones}: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Text(
l10n.noDronesYet,
style: Theme.of(context).textTheme.titleSmall,
),
);
} else {
final drones = snapshot.data!;
return DropdownButtonHideUnderline(
child: DropdownButton<Drone>(
isExpanded: true,
value: selectedDrone,
hint: Text(
l10n.selectDroneHint,
style: Theme.of(context).textTheme.bodyLarge,
),
dropdownColor: Theme.of(context).cardTheme.color,
style: Theme.of(context).textTheme.titleMedium,
icon: const Icon(Icons.arrow_drop_down, color: Colors.white70),
onChanged: isFlightActive || isGettingLocation
? null
: (Drone? newValue) {
setState(() {
selectedDrone = newValue;
});
},
items: drones.map<DropdownMenuItem<Drone>>((Drone drone) {
return DropdownMenuItem<Drone>(
value: drone,
child: Text(drone.name),
);
}).toList(),
),
);
}
},
),
),
),
Text(
l10n.chooseBattery,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 15),
Card(
margin: const EdgeInsets.only(bottom: 30),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: FutureBuilder<List<Battery>>(
future: _batteriesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('${l10n.errorLoadingBatteries}: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Text(
l10n.noBatteriesYet,
style: Theme.of(context).textTheme.titleSmall,
),
);
} else {
final batteries = snapshot.data!;
return DropdownButtonHideUnderline(
child: DropdownButton<Battery>(
isExpanded: true,
value: selectedBattery,
hint: Text(
l10n.selectBatteryHint,
style: Theme.of(context).textTheme.bodyLarge,
),
dropdownColor: Theme.of(context).cardTheme.color,
style: Theme.of(context).textTheme.titleMedium,
icon: const Icon(Icons.arrow_drop_down, color: Colors.white70),
onChanged: isFlightActive || isGettingLocation
? null
: (Battery? newValue) {
setState(() {
selectedBattery = newValue;
});
},
items: batteries.map<DropdownMenuItem<Battery>>((Battery battery) {
return DropdownMenuItem<Battery>(
value: battery,
child: Text('${battery.name} (${battery.voltage}V)'),
);
}).toList(),
),
);
}
},
),
),
),
Card(
margin: const EdgeInsets.only(bottom: 30),
child: ListTile(
title: Text(l10n.recordFlightLocation, style: Theme.of(context).textTheme.titleMedium),
trailing: Switch(
value: useGpsLocation,
onChanged: isFlightActive || isGettingLocation
? null
: (bool value) async {
setState(() {
useGpsLocation = value;
});
if (value) {
await _getCurrentLocation();
} else {
setState(() {
currentPosition = null;
_initialMapCentered = false;
});
}
},
activeColor: Theme.of(context).colorScheme.primary,
),
leading: Icon(Icons.location_on, color: Theme.of(context).colorScheme.secondary),
),
),
if (currentPosition != null && useGpsLocation)
Container(
margin: const EdgeInsets.only(bottom: 20),
height: 250,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 2,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: LatLng(currentPosition!.latitude, currentPosition!.longitude),
initialZoom: 17.0,
maxZoom: 19.0,
minZoom: 3.0,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
onMapReady: () {
if (!_initialMapCentered && currentPosition != null) {
_mapController.move(
LatLng(currentPosition!.latitude, currentPosition!.longitude),
_mapController.camera.zoom,
);
setState(() {
_initialMapCentered = true;
});
}
},
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.rtime',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(currentPosition!.latitude, currentPosition!.longitude),
width: 80,
height: 80,
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 40,
),
),
],
),
],
),
),
),
Center(
child: Text(
formattedTime,
style: const TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
color: Colors.lightBlueAccent,
fontFamily: 'RobotoMono',
),
),
),
const SizedBox(height: 40),
Center(
child: isFlightActive
? ElevatedButton.icon(
onPressed: _stopFlight,
icon: const Icon(Icons.stop, size: 30),
label: Text(
l10n.stopFlight,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
minimumSize: const Size(250, 70),
),
)
: (isGettingLocation
? Column(
children: [
const CircularProgressIndicator(color: Colors.tealAccent),
const SizedBox(height: 10),
Text(l10n.obtainingLocation, style: Theme.of(context).textTheme.titleSmall),
],
)
: ElevatedButton.icon(
onPressed: _startFlight,
icon: const Icon(Icons.flight_takeoff, size: 30),
label: Text(
l10n.startFlight,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.tealAccent,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
minimumSize: const Size(250, 70),
),
)),
),
const SizedBox(height: 20),
],
),
),
);
}
}

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:rtime/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:rtime/providers/local_provider.dart';
import 'package:rtime/providers/theme_provider.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final currentLocale = Localizations.localeOf(context);
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
appBar: AppBar(
title: Text(l10n.settingsTitle),
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
foregroundColor: Theme.of(context).appBarTheme.foregroundColor,
iconTheme: Theme.of(context).iconTheme,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.languageSetting,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
Card(
color: Theme.of(context).cardTheme.color,
child: Column(
children: [
ListTile(
title: Text(l10n.english, style: Theme.of(context).textTheme.titleMedium),
trailing: currentLocale.languageCode == 'en'
? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary)
: null,
onTap: () {
final localeProvider = Provider.of<LocaleProvider>(context, listen: false);
localeProvider.setLocale(const Locale('en', ''));
},
),
ListTile(
title: Text(l10n.french, style: Theme.of(context).textTheme.titleMedium),
trailing: currentLocale.languageCode == 'fr'
? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary)
: null,
onTap: () {
final localeProvider = Provider.of<LocaleProvider>(context, listen: false);
localeProvider.setLocale(const Locale('fr', ''));
},
),
],
),
),
const SizedBox(height: 30),
Text(
l10n.themeSetting,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
Card(
color: Theme.of(context).cardTheme.color,
child: Column(
children: [
ListTile(
title: Text(l10n.themeSystem, style: Theme.of(context).textTheme.titleMedium),
trailing: themeProvider.themeMode == ThemeMode.system
? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary)
: null,
onTap: () {
themeProvider.setThemeMode(ThemeMode.system);
},
),
ListTile(
title: Text(l10n.themeLight, style: Theme.of(context).textTheme.titleMedium),
trailing: themeProvider.themeMode == ThemeMode.light
? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary)
: null,
onTap: () {
themeProvider.setThemeMode(ThemeMode.light);
},
),
ListTile(
title: Text(l10n.themeDark, style: Theme.of(context).textTheme.titleMedium),
trailing: themeProvider.themeMode == ThemeMode.dark
? Icon(Icons.check_circle, color: Theme.of(context).colorScheme.secondary)
: null,
onTap: () {
themeProvider.setThemeMode(ThemeMode.dark);
},
),
],
),
),
],
),
),
);
}
}