Compare commits
3 Commits
b35656d4ce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6735ddb5fd | |||
| 3119e5fdbf | |||
| 5d461d5b0f |
@ -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"
|
||||
|
||||
7
android/app/src/main/res/values-v35/styles.xml
Normal file
7
android/app/src/main/res/values-v35/styles.xml
Normal 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
150
lib/main.dart
150
lib/main.dart
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
24
pubspec.lock
24
pubspec.lock
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user