Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup
run: |
flutter pub get
flutter pub run build_runner build
flutter pub run build_runner build --delete-conflicting-outputs
dart pub global activate protoc_plugin
chmod +x generate_protos.sh
./generate_protos.sh
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ migrate_working_dir/
coverage
*.mocks.dart
*.pb*.dart
*.g.dart

# Web related
lib/generated_plugin_registrant.dart
Expand Down
4 changes: 3 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ analyzer:
missing_enum_constant_in_switch: error
exhaustive_cases: error
exclude:
- lib/**.pb*.dart
- lib/**.pb*.dart
- lib/data/*.g.dart
- test/**
10 changes: 10 additions & 0 deletions assets/svg/ic_edit_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions lib/data/model/project.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:floor/floor.dart';

@entity
class Project {
String name;
String path;
DateTime lastModified;
DateTime creationDate;
String? resolution;
String? format;
int? size;
String? imagePreviewPath;
@PrimaryKey(autoGenerate: true)
final int? id;

Project({
required this.name,
required this.path,
required this.lastModified,
required this.creationDate,
this.resolution,
this.format,
this.size,
this.imagePreviewPath,
this.id,
});
}
21 changes: 21 additions & 0 deletions lib/data/project_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:floor/floor.dart';

import 'model/project.dart';

@dao
abstract class ProjectDAO {
@Insert(onConflict: OnConflictStrategy.replace)
Future<int> insertProject(Project project);

@Insert(onConflict: OnConflictStrategy.replace)
Future<List<int>> insertProjects(List<Project> projects);

@Query('DELETE FROM Project WHERE id = :id')
Future<void> deleteProject(int id);

@delete
Future<void> deleteProjects(List<Project> projects);

@Query('SELECT * FROM Project order by lastModified desc')
Future<List<Project>> getProjects();
}
19 changes: 19 additions & 0 deletions lib/data/project_database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'dart:async';

import 'package:floor/floor.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:paintroid/data/model/project.dart';
import 'package:paintroid/data/project_dao.dart';
import 'package:paintroid/data/typeconverters/date_time_converter.dart';
import 'package:sqflite/sqflite.dart' as sqflite;

part 'project_database.g.dart';

@TypeConverters([DateTimeConverter])
@Database(version: 1, entities: [Project])
abstract class ProjectDatabase extends FloorDatabase {
ProjectDAO get projectDAO;

static final provider = FutureProvider((ref) =>
$FloorProjectDatabase.databaseBuilder("project_database.db").build());
}
13 changes: 13 additions & 0 deletions lib/data/typeconverters/date_time_converter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:floor/floor.dart';

class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}

@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
34 changes: 34 additions & 0 deletions lib/io/src/service/file_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import 'package:oxidized/oxidized.dart';
import 'package:paintroid/core/failure.dart';
import 'package:paintroid/core/loggable_mixin.dart';
import 'package:paintroid/io/io.dart';
import 'package:path_provider/path_provider.dart';

abstract class IFileService {
Future<Result<File, Failure>> save(String filename, Uint8List data);

Future<Result<File, Failure>> saveToApplicationDirectory(
String filename, Uint8List data);

Future<Result<File, Failure>> pick();

Result<File, Failure> getFile(String path);

static final provider = Provider<IFileService>((ref) => FileService());
}

Expand Down Expand Up @@ -51,4 +57,32 @@ class FileService with LoggableMixin implements IFileService {
return Result.err(SaveImageFailure.unidentified);
}
}

Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}

@override
Future<Result<File, Failure>> saveToApplicationDirectory(
String filename, Uint8List data) async {
try {
String saveDirectory = "${await _localPath}/$filename";
final file = await File(saveDirectory).create(recursive: true);
return Result.ok(await file.writeAsBytes(data));
} catch (err, stacktrace) {
logger.severe("Could not save file", err, stacktrace);
return Result.err(SaveImageFailure.unidentified);
}
}

@override
Result<File, Failure> getFile(String path) {
try {
return Result.ok(File(path));
} catch (err, stacktrace) {
logger.severe("Could not load file", err, stacktrace);
return Result.err(LoadImageFailure.unidentified);
}
}
}
15 changes: 15 additions & 0 deletions lib/io/src/service/image_service.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

Expand All @@ -19,6 +20,8 @@ abstract class IImageService {

Future<Result<Uint8List, Failure>> exportAsPng(ui.Image image);

Result<Uint8List, Failure> getProjectPreview(String? path);

static final provider = Provider<IImageService>((ref) => ImageService());
}

Expand Down Expand Up @@ -61,4 +64,16 @@ class ImageService with LoggableMixin implements IImageService {
return Result.err(SaveImageFailure.unidentified);
}
}

@override
Result<Uint8List, Failure> getProjectPreview(String? path) {
try {
if (path == null) throw "Unable to get the project preview";
final file = File(path);
return Result.ok(file.readAsBytesSync());
} catch (err, stacktrace) {
logger.severe("Could not get the project preview", err, stacktrace);
return Result.err(LoadImageFailure.invalidImage);
}
}
}
39 changes: 39 additions & 0 deletions lib/io/src/ui/delete_project_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';

/// Returns [true] if user chose to delete the project or [null] if user
/// dismiss the dialog by tapping outside
Future<bool?> showDeleteDialog(BuildContext context, String name) =>
showGeneralDialog<bool>(
context: context,
pageBuilder: (_, __, ___) => DeleteProjectDialog(name: name),
barrierDismissible: true,
barrierLabel: "Show delete project dialog box");

class DeleteProjectDialog extends StatefulWidget {
final String name;

const DeleteProjectDialog({Key? key, required this.name}) : super(key: key);

@override
State<DeleteProjectDialog> createState() => _DeleteProjectDialogState();
}

class _DeleteProjectDialogState extends State<DeleteProjectDialog> {
@override
Widget build(BuildContext context) => AlertDialog(
title: Text("Delete ${widget.name}"),
actions: [_discardButton, _deleteButton],
content: const Text("Do you really want to delete your project?"),
);

TextButton get _deleteButton => TextButton(
style: TextButton.styleFrom(primary: Colors.red),
onPressed: () => Navigator.of(context).pop(true),
child: const Text("Delete"),
);

ElevatedButton get _discardButton => ElevatedButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Cancel", style: TextStyle(color: Colors.white)),
);
}
110 changes: 110 additions & 0 deletions lib/io/src/ui/project_details_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart';
import 'package:intl/intl.dart';
import 'package:oxidized/oxidized.dart';
import 'package:paintroid/io/io.dart';

import '../../../data/model/project.dart';
import '../../../ui/color_schemes.dart';

Future<bool?> showDetailsDialog(BuildContext context, Project project) =>
showGeneralDialog<bool>(
context: context,
pageBuilder: (_, __, ___) => ProjectDetailsDialog(project: project),
barrierDismissible: true,
barrierLabel: "Show project details dialog box");

class ProjectDetailsDialog extends ConsumerStatefulWidget {
final Project project;

const ProjectDetailsDialog({Key? key, required this.project})
: super(key: key);

@override
ConsumerState<ProjectDetailsDialog> createState() =>
_ProjectDetailsDialogState();
}

class _ProjectDetailsDialogState extends ConsumerState<ProjectDetailsDialog> {
late IImageService imageService;
late IFileService fileService;

@override
Widget build(BuildContext context) {
imageService = ref.watch(IImageService.provider);
fileService = ref.watch(IFileService.provider);

final DateFormat formatter = DateFormat('dd-MM-yyyy HH:mm:ss');

return AlertDialog(
title: Text(widget.project.name),
actions: [_okButton],
content: FutureBuilder(
future: _getImageDimensions(widget.project.imagePreviewPath),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
final dimensions = snapshot.data!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Resolution: ${dimensions[0]} X ${dimensions[1]}"),
Text(
"Last modified: ${formatter.format(widget.project.lastModified)}"),
Text(
"Creation date: ${formatter.format(widget.project.creationDate)}"),
Text("Size: ${filesize(_getProjectSize())}"),
],
);
} else {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
backgroundColor: lightColorScheme.background,
),
],
);
}
},
),
);
}

ElevatedButton get _okButton => ElevatedButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("OK", style: TextStyle(color: Colors.white)),
);

int _getProjectSize() => fileService.getFile(widget.project.path).when(
ok: (file) => file.lengthSync(),
err: (failure) {
showToast(failure.message);
return 0;
},
);

Future<List<int>> _getImageDimensions(String? path) async {
List<int> dimensions = [];
return imageService.getProjectPreview(path).when(
ok: (img) => imageService.import(img).when(
ok: (image) {
dimensions.add(image.width);
dimensions.add(image.height);
return dimensions;
},
err: (failure) {
showToast(failure.message);
return dimensions;
},
),
err: (failure) {
showToast(failure.message);
return dimensions;
},
);
}
}
Loading