Link between front-end and back-end
This commit is contained in:
425
lib/pages/battery_detail_page.dart
Normal file
425
lib/pages/battery_detail_page.dart
Normal 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());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
367
lib/pages/drone_detail_page.dart
Normal file
367
lib/pages/drone_detail_page.dart
Normal 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());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
373
lib/pages/flight_detail_page.dart
Normal file
373
lib/pages/flight_detail_page.dart
Normal 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
389
lib/pages/home_page.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
252
lib/pages/new_battery_page.dart
Normal file
252
lib/pages/new_battery_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
200
lib/pages/new_drone_page.dart
Normal file
200
lib/pages/new_drone_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
578
lib/pages/new_flight_page.dart
Normal file
578
lib/pages/new_flight_page.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/pages/settings_page.dart
Normal file
126
lib/pages/settings_page.dart
Normal 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);
|
||||
|
||||
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user