Skip to content

Commit

Permalink
feat: 5301 - added an erasing tool for proofs (#5341)
Browse files Browse the repository at this point in the history
New files:
* `eraser_model.dart`: Model about the eraser tool: coordinate computations.
* `eraser_painter.dart`:  Painter of the eraser tool: displaying thick lines.

Impacted files:
* `background_task_add_price.dart`: displaying the eraser bars if relevant; new `eraserCoordinates` field
* `background_task_image.dart`: minor refactoring
* `crop_helper.dart`: new `enableEraser` field `offsets` parameters
* `crop_page.dart`: added an erasing tool for proofs; refactored
* `crop_parameters.dart`: new `eraserCoordinates` parameter
* `operation_type.dart` unrelated minor refactoring
* `product_crop_helper.dart`: minor refactoring
* `product_price_item.dart`: unrelated fix for TEST env
* `proof_crop_helper.dart`: minor refactoring
* `pubspec.lock`: wtf
* `pubspec.yaml`: needed upgrade of `crop_image` to `1.0.13`
  • Loading branch information
monsieurtanuki authored Jun 7, 2024
1 parent d5bfb6a commit 036bda1
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 89 deletions.
46 changes: 46 additions & 0 deletions packages/smooth_app/lib/background/background_task_add_price.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:crop_image/crop_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http_parser/http_parser.dart';
Expand All @@ -11,6 +12,8 @@ import 'package:smooth_app/background/background_task_upload.dart';
import 'package:smooth_app/background/operation_type.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/pages/crop_parameters.dart';
import 'package:smooth_app/pages/prices/eraser_model.dart';
import 'package:smooth_app/pages/prices/eraser_painter.dart';

// TODO(monsieurtanuki): use transient file, in order to have instant access to proof image?
// TODO(monsieurtanuki): add source
Expand All @@ -33,6 +36,8 @@ class BackgroundTaskAddPrice extends BackgroundTask {
required this.currency,
required this.locationOSMId,
required this.locationOSMType,
// lines
required this.eraserCoordinates,
// multi
required this.barcode,
required this.priceIsDiscounted,
Expand All @@ -53,12 +58,25 @@ class BackgroundTaskAddPrice extends BackgroundTask {
locationOSMId = json[_jsonTagOSMId] as int,
locationOSMType =
LocationOSMType.fromOffTag(json[_jsonTagOSMType] as String)!,
eraserCoordinates =
_fromJsonListDouble(json[_jsonTagEraserCoordinates]),
barcode = json[_jsonTagBarcode] as String,
priceIsDiscounted = json[_jsonTagIsDiscounted] as bool,
price = json[_jsonTagPrice] as double,
priceWithoutDiscount = json[_jsonTagPriceWithoutDiscount] as double?,
super.fromJson(json);

static List<double>? _fromJsonListDouble(final List<dynamic>? input) {
if (input == null) {
return null;
}
final List<double> result = <double>[];
for (final dynamic item in input) {
result.add(item as double);
}
return result;
}

static const String _jsonTagImagePath = 'imagePath';
static const String _jsonTagRotation = 'rotation';
static const String _jsonTagX1 = 'x1';
Expand All @@ -67,6 +85,7 @@ class BackgroundTaskAddPrice extends BackgroundTask {
static const String _jsonTagY2 = 'y2';
static const String _jsonTagProofType = 'proofType';
static const String _jsonTagDate = 'date';
static const String _jsonTagEraserCoordinates = 'eraserCoordinates';
static const String _jsonTagCurrency = 'currency';
static const String _jsonTagOSMId = 'osmId';
static const String _jsonTagOSMType = 'osmType';
Expand All @@ -88,6 +107,7 @@ class BackgroundTaskAddPrice extends BackgroundTask {
final Currency currency;
final int locationOSMId;
final LocationOSMType locationOSMType;
final List<double>? eraserCoordinates;
final String barcode;
final bool priceIsDiscounted;
final double price;
Expand All @@ -107,6 +127,7 @@ class BackgroundTaskAddPrice extends BackgroundTask {
result[_jsonTagCurrency] = currency.name;
result[_jsonTagOSMId] = locationOSMId;
result[_jsonTagOSMType] = locationOSMType.offTag;
result[_jsonTagEraserCoordinates] = eraserCoordinates;
result[_jsonTagBarcode] = barcode;
result[_jsonTagIsDiscounted] = priceIsDiscounted;
result[_jsonTagPrice] = price;
Expand Down Expand Up @@ -185,6 +206,7 @@ class BackgroundTaskAddPrice extends BackgroundTask {
currency: currency,
locationOSMId: locationOSMId,
locationOSMType: locationOSMType,
eraserCoordinates: cropObject.eraserCoordinates,
barcode: barcode,
priceIsDiscounted: priceIsDiscounted,
price: price,
Expand Down Expand Up @@ -238,6 +260,16 @@ class BackgroundTaskAddPrice extends BackgroundTask {
..priceWithoutDiscount = priceWithoutDiscount
..productCode = barcode;

final List<Offset> offsets = <Offset>[];
if (eraserCoordinates != null) {
for (int i = 0; i < eraserCoordinates!.length; i += 2) {
final Offset offset = Offset(
eraserCoordinates![i],
eraserCoordinates![i + 1],
);
offsets.add(offset);
}
}
final String? path = await BackgroundTaskImage.cropIfNeeded(
fullPath: fullPath,
croppedPath: BackgroundTaskImage.getCroppedPath(fullPath),
Expand All @@ -246,6 +278,20 @@ class BackgroundTaskAddPrice extends BackgroundTask {
cropY1: cropY1,
cropX2: cropX2,
cropY2: cropY2,
overlayPainter: offsets.isEmpty
? null
: EraserPainter(
eraserModel: EraserModel(
rotation: CropRotationExtension.fromDegrees(rotationDegrees)!,
offsets: offsets,
),
cropRect: BackgroundTaskImage.getDownsizedRect(
cropX1,
cropY1,
cropX2,
cropY2,
),
),
);
if (path == null) {
// TODO(monsieurtanuki): maybe something more refined when we dismiss the picture, like alerting the user, though it's not supposed to happen anymore from upstream.
Expand Down
12 changes: 8 additions & 4 deletions packages/smooth_app/lib/background/background_task_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class BackgroundTaskImage extends BackgroundTaskUpload {
static Rect getUpsizedRect(final Rect source) =>
getResizedRect(source, _cropConversionFactor);

static Rect _getDownsizedRect(
static Rect getDownsizedRect(
final int cropX1,
final int cropY1,
final int cropX2,
Expand Down Expand Up @@ -234,6 +234,7 @@ class BackgroundTaskImage extends BackgroundTaskUpload {
required final int cropY1,
required final int cropX2,
required final int cropY2,
final CustomPainter? overlayPainter,
}) async {
final ui.Image full = await loadUiImage(
await (await BackgroundTaskUpload.getFile(fullPath)).readAsBytes());
Expand All @@ -246,11 +247,13 @@ class BackgroundTaskImage extends BackgroundTaskUpload {
return null;
}
// in that case, no need to crop
return fullPath;
if (overlayPainter == null) {
return fullPath;
}
}

Size getCroppedSize() {
final Rect cropRect = _getDownsizedRect(cropX1, cropY1, cropX2, cropY2);
final Rect cropRect = getDownsizedRect(cropX1, cropY1, cropX2, cropY2);
switch (CropRotationExtension.fromDegrees(rotationDegrees)!) {
case CropRotation.up:
case CropRotation.down:
Expand All @@ -272,11 +275,12 @@ class BackgroundTaskImage extends BackgroundTaskUpload {
return null;
}
final ui.Image cropped = await CropController.getCroppedBitmap(
crop: _getDownsizedRect(cropX1, cropY1, cropX2, cropY2),
crop: getDownsizedRect(cropX1, cropY1, cropX2, cropY2),
rotation: CropRotationExtension.fromDegrees(rotationDegrees)!,
image: full,
maxSize: null,
quality: FilterQuality.high,
overlayPainter: overlayPainter,
);
await saveJpeg(
file: await BackgroundTaskUpload.getFile(croppedPath),
Expand Down
87 changes: 31 additions & 56 deletions packages/smooth_app/lib/background/operation_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,66 +67,41 @@ enum OperationType {
'$_transientHeaderSeparator${work ?? ''}';
}

BackgroundTask fromJson(Map<String, dynamic> map) {
switch (this) {
case crop:
return BackgroundTaskCrop.fromJson(map);
case addPrice:
return BackgroundTaskAddPrice.fromJson(map);
case details:
return BackgroundTaskDetails.fromJson(map);
case hungerGames:
return BackgroundTaskHungerGames.fromJson(map);
case image:
return BackgroundTaskImage.fromJson(map);
case refreshLater:
return BackgroundTaskRefreshLater.fromJson(map);
case unselect:
return BackgroundTaskUnselect.fromJson(map);
case offline:
return BackgroundTaskOffline.fromJson(map);
case offlineBarcodes:
return BackgroundTaskTopBarcodes.fromJson(map);
case offlineProducts:
return BackgroundTaskDownloadProducts.fromJson(map);
case fullRefresh:
return BackgroundTaskFullRefresh.fromJson(map);
case languageRefresh:
return BackgroundTaskLanguageRefresh.fromJson(map);
}
}
BackgroundTask fromJson(Map<String, dynamic> map) => switch (this) {
crop => BackgroundTaskCrop.fromJson(map),
addPrice => BackgroundTaskAddPrice.fromJson(map),
details => BackgroundTaskDetails.fromJson(map),
hungerGames => BackgroundTaskHungerGames.fromJson(map),
image => BackgroundTaskImage.fromJson(map),
refreshLater => BackgroundTaskRefreshLater.fromJson(map),
unselect => BackgroundTaskUnselect.fromJson(map),
offline => BackgroundTaskOffline.fromJson(map),
offlineBarcodes => BackgroundTaskTopBarcodes.fromJson(map),
offlineProducts => BackgroundTaskDownloadProducts.fromJson(map),
fullRefresh => BackgroundTaskFullRefresh.fromJson(map),
languageRefresh => BackgroundTaskLanguageRefresh.fromJson(map),
};

bool matches(final TransientOperation action) =>
action.key.startsWith('$header$_transientHeaderSeparator');

String getLabel(final AppLocalizations appLocalizations) {
switch (this) {
case OperationType.details:
return appLocalizations.background_task_operation_details;
case OperationType.addPrice:
return 'Add price';
case OperationType.image:
return appLocalizations.background_task_operation_image;
case OperationType.unselect:
return 'Unselect a product image';
case OperationType.hungerGames:
return 'Answering to a Hunger Games question';
case OperationType.crop:
return 'Crop an existing image';
case OperationType.refreshLater:
return 'Waiting 10 min before refreshing product to get all automatic edits';
case OperationType.offline:
return 'Downloading top n products for offline usage';
case OperationType.offlineBarcodes:
return 'Downloading top n barcodes';
case OperationType.offlineProducts:
return 'Downloading products';
case OperationType.fullRefresh:
return 'Refreshing the full local database';
case OperationType.languageRefresh:
return 'Refreshing the local database to a new language';
}
}
String getLabel(final AppLocalizations appLocalizations) => switch (this) {
OperationType.details =>
appLocalizations.background_task_operation_details,
OperationType.addPrice => 'Add price',
OperationType.image => appLocalizations.background_task_operation_image,
OperationType.unselect => 'Unselect a product image',
OperationType.hungerGames => 'Answering to a Hunger Games question',
OperationType.crop => 'Crop an existing image',
OperationType.refreshLater =>
'Waiting 10 min before refreshing product to get all automatic edits',
OperationType.offline => 'Downloading top n products for offline usage',
OperationType.offlineBarcodes => 'Downloading top n barcodes',
OperationType.offlineProducts => 'Downloading products',
OperationType.fullRefresh => 'Refreshing the full local database',
OperationType.languageRefresh =>
'Refreshing the local database to a new language',
};

static int getSequentialId(final TransientOperation operation) {
final List<String> keyItems =
Expand Down
13 changes: 13 additions & 0 deletions packages/smooth_app/lib/pages/crop_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ abstract class CropHelper {
required final File smallCroppedFile,
required final Directory directory,
required final int sequenceNumber,
final List<Offset>? offsets,
});

/// Should we display the eraser with the crop grid?
bool get enableEraser;

/// Returns the crop rect according to local cropping method * factor.
@protected
Rect getLocalCropRect(final CropController controller) =>
Expand All @@ -43,8 +47,16 @@ abstract class CropHelper {
required final CropController controller,
required final File? fullFile,
required final File smallCroppedFile,
final List<Offset>? offsets,
}) {
final Rect cropRect = getLocalCropRect(controller);
final List<double> eraserCoordinates = <double>[];
if (offsets != null) {
for (final Offset offset in offsets) {
eraserCoordinates.add(offset.dx);
eraserCoordinates.add(offset.dy);
}
}
return CropParameters(
fullFile: fullFile,
smallCroppedFile: smallCroppedFile,
Expand All @@ -53,6 +65,7 @@ abstract class CropHelper {
y1: cropRect.top.ceil(),
x2: cropRect.right.floor(),
y2: cropRect.bottom.floor(),
eraserCoordinates: eraserCoordinates,
);
}

Expand Down
Loading

0 comments on commit 036bda1

Please sign in to comment.