Compare commits

...

3 Commits

9 changed files with 177 additions and 203 deletions

View File

@ -3,6 +3,10 @@
android:label="rtime"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Ucrop.CropTheme"/>
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

View File

@ -15,4 +15,5 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar"/> <!--add this line-->
</resources>

View File

@ -1,6 +1,12 @@
// This file holds a class to represent the configuration/settings of the app
import 'dart:io';
import 'package:json_annotation/json_annotation.dart';
import 'dart:convert';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
part 'generated/config.g.dart';
@JsonSerializable()
@ -10,7 +16,24 @@ class RTimeConfig {
RTimeConfig({required this.language});
factory RTimeConfig.fromJson(Map<String, dynamic> json) =>
_$PersonFromJson(json);
_$RTimeConfigFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
Map<String, dynamic> toJson() => _$RTimeConfigToJson(this);
static Future<RTimeConfig?> readFromConfig() async {
final directory = await getApplicationDocumentsDirectory();
final configName = "rtime_config.json";
final configPath = path.join(directory.path, configName);
final string = jsonDecode(await File(configPath).readAsString());
return RTimeConfig.fromJson(string);
}
void writeConfig() async {
final directory = await getApplicationDocumentsDirectory();
final configName = "rtime_config.json";
final configPath = path.join(directory.path, configName);
await File(configPath).writeAsString(jsonEncode(toJson()));
}
}

View File

@ -160,6 +160,34 @@ class DbHelper {
return maps.map((e) => Flight.fromMap(e)).toList();
}
Future<List<Flight>> getFlightsBetween(
int startTimestamp,
int endTimestamp,
) async {
final db = await database;
final maps = await db.rawQuery('''
SELECT * FROM flights AS F
WHERE F.start_timestamp >= $startTimestamp AND F.end_timestamp <= $endTimestamp
''');
return maps.map((e) => Flight.fromMap(e)).toList();
}
Future<int?> getTotalFlightTime() async {
final db = await database;
final ft = await db.rawQuery('''
SELECT SUM(F.end_timestamp - F.startTimestamp) FROM flights AS F
''');
final mapping = ft.firstOrNull;
if (mapping == null) {
return null;
}
final val = mapping["SUM(F.end_timestamp - F.startTimestamp)"];
if (val is! int) {
return null;
}
return val;
}
Future closeDb() async {
final db = await database;
log.info("Closing database.");

View File

@ -7,73 +7,97 @@ import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:uuid/uuid.dart';
import 'package:image/image.dart' as img;
import 'package:image_cropper/image_cropper.dart' as img_cropper;
import 'package:uuid/v5.dart';
class ImagesManager
{
static final ImagesManager instance = ImagesManager._internal();
ImagesManager._internal();
class ImagesManager {
static final ImagesManager instance = ImagesManager._internal();
ImagesManager._internal();
static final Logger log = Logger("ImagesManager");
static final Logger log = Logger("ImagesManager");
static Uri? _imagesDirectory;
static Uri? _imagesDirectory;
Future<Uri> get imageDirectory async
{
if(_imagesDirectory != null) return _imagesDirectory!;
await _initImagesDirectory();
return _imagesDirectory!;
Future<Uri> get imageDirectory async {
if (_imagesDirectory != null) return _imagesDirectory!;
await _initImagesDirectory();
return _imagesDirectory!;
}
Future _initImagesDirectory() async {
final directoryLoc = await getApplicationDocumentsDirectory();
final directoryName = "images";
final directoryPath = path.join(directoryLoc.path, directoryName);
final directoryUri = Uri.directory(directoryPath);
final directory = Directory.fromUri(directoryUri);
if (!await directory.exists()) {
log.info("Image directory does not yet extists. Creating it.");
}
Future _initImagesDirectory() async
{
final directoryLoc = await getApplicationDocumentsDirectory();
final directoryName = "images";
final directoryPath = path.join(directoryLoc.path, directoryName);
final directoryUri = Uri.directory(directoryPath);
final directory = Directory.fromUri(directoryUri);
if(!await directory.exists())
{
log.info("Image directory does not yet extists. Creating it.");
}
directory.create(recursive: false);
directory.create(recursive: false);
log.info("Image directory set up at '$directory'");
_imagesDirectory = directoryUri;
log.info("Image directory set up at '$directory'");
_imagesDirectory = directoryUri;
}
Future<String?> createImage(ImageSource source) async {
// Get image from camera or not
final XFile? ximage = await ImagePicker().pickImage(source: source);
if (ximage == null) return null;
// Crop image
final uuid = Uuid().v6();
final imageDir = await imageDirectory;
final finalPath = path.join(
imageDir.path,
uuid + path.extension(ximage.name),
);
final tempPath = path.join(
imageDir.path,
"${uuid}_tocrop${path.extension(ximage.name)}",
);
await ximage.saveTo(tempPath);
img_cropper.CroppedFile? cropped = await img_cropper.ImageCropper()
.cropImage(
sourcePath: tempPath,
aspectRatio: img_cropper.CropAspectRatio(ratioX: 1.0, ratioY: 1.0),
uiSettings: [
img_cropper.AndroidUiSettings(
toolbarTitle: "Crop image",
toolbarColor: Colors.black,
toolbarWidgetColor: Colors.white,
),
img_cropper.IOSUiSettings(title: "Crop image"),
],
);
if (cropped == null) return null;
await File(finalPath).writeAsBytes(await cropped.readAsBytes());
await File(tempPath).delete();
return uuid;
}
Future<Image?> loadImage(String imageUuid) async {
final imageDir = await imageDirectory;
if (!Uuid.isValidUUID(fromString: imageUuid)) {
log.warning(
"Tried to load an image with an invalid UUID : '$imageUuid'.",
);
return null;
}
Future<String?> createImage(ImageSource source) async
{
// Get image from camera or not
final XFile? ximage = await ImagePicker().pickImage(source: source);
if(ximage == null) return null;
final uuid = Uuid().v6();
final imageDir = await imageDirectory;
ximage.saveTo(path.join(imageDir.path, uuid + path.extension(ximage.name)));
return uuid;
final imagePath = path.join(imageDir.path, "$imageUuid.jpg");
final file = File(imagePath);
if (!await file.exists()) {
log.warning("Tried to load an image that does not extist: '$imagePath'.");
return null;
}
Future<Image?> loadImage(String imageUuid) async
{
final imageDir = await imageDirectory;
if(!Uuid.isValidUUID(fromString: imageUuid))
{
log.warning("Tried to load an image with an invalid UUID : '$imageUuid'.");
return null;
}
final imagePath = path.join(imageDir.path, "$imageUuid.jpg");
final file = File(imagePath);
if(!await file.exists())
{
log.warning("Tried to load an image that does not extist: '$imagePath'.");
return null;
}
return Image.file(file);
}
return Image.file(file);
}
}

View File

@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
import 'package:rtime/db/db_helper.dart';
import 'package:rtime/images_manager.dart';
import 'package:rtime/models/drone.dart';
import 'package:rtime/pages/home_page.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
@ -21,156 +22,17 @@ void main() {
(record) =>
print('${record.level.name}: ${record.time}: ${record.message}'),
);
runApp(const MyApp());
runApp(const RTimeApp());
//DbHelper.instance.closeDb();
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
class RTimeApp extends StatelessWidget {
const RTimeApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
Future _incrementCounter() async {
DbHelper.instance.insertDrone(
Drone(
name: "Image test",
imageUuid: await ImagesManager.instance.createImage(ImageSource.camera),
),
);
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
var children = <Widget>[
const Text('You have pushed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headlineMedium),
FutureBuilder(
future: Future<Widget>(() async {
final drones = await DbHelper.instance.getDrones();
if (drones.isEmpty) {
return Icon(Icons.question_mark);
}
final image = await ImagesManager.instance.loadImage(
drones.first.imageUuid!,
);
if (image == null) {
return Icon(Icons.error);
}
return image;
}),
builder: (BuildContext ctx, AsyncSnapshot<Widget> img) {
if (!img.hasData) {
return Center(child: CircularProgressIndicator());
}
return img.data!;
},
),
Icon(Icons.build),
];
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
return Text("RtimeAPP");
}
}

View File

@ -344,6 +344,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_cropper:
dependency: "direct main"
description:
name: image_cropper
sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
image_cropper_for_web:
dependency: transitive
description:
name: image_cropper_for_web
sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
url: "https://pub.dev"
source: hosted
version: "6.1.0"
image_cropper_platform_interface:
dependency: transitive
description:
name: image_cropper_platform_interface
sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
image_picker:
dependency: "direct main"
description:

View File

@ -44,6 +44,7 @@ dependencies:
image_picker: ^1.1.2
uuid: ^4.5.1
json_serializable: ^6.9.5
image_cropper: ^9.1.0
dev_dependencies:
flutter_test: