Skip to content

Commit 5a88d11

Browse files
feat: 6289 - faster bulk proof upload without cropping (#6486)
* feat: 6289 - faster bulk proof upload without cropping New file: * `price_bulk_proof_card.dart`: Card that displays the bulk proof button for price adding. Impacted files: * `crop_helper.dart`: minor refactoring * `crop_page.dart`: minor refactoring * `crop_parameters.dart`: minor refactoring * `eraser_model.dart`: minor refactoring * `price_proof_card.dart`: minor refactoring * `proof_bulk_add_page.dart`: simplified by removing the FAB and using new widget `PriceBulkProofCard` * added warning * added l10n
1 parent b3b9ed8 commit 5a88d11

File tree

8 files changed

+208
-147
lines changed

8 files changed

+208
-147
lines changed

packages/smooth_app/lib/l10n/app_en.arb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2278,8 +2278,10 @@
22782278
},
22792279
"prices_app_dev_mode_flag": "Shortcut to Prices app on product page",
22802280
"prices_app_button": "Go to Prices app",
2281+
"prices_bulk_proof_upload_select": "Add price tags directly from gallery",
2282+
"prices_bulk_proof_upload_warning": "Once you've selected images, you won't be able to edit them!",
2283+
"prices_bulk_proof_upload_subtitle": "Multiple Price Tags",
22812284
"prices_bulk_proof_upload_title": "Bulk Proof Upload",
2282-
"prices_bulk_proof_upload_action": "Send the proof",
22832285
"prices_generic_title": "Prices",
22842286
"prices_add_n_prices": "{count,plural, =1{Add a price} other{Add {count} prices}}",
22852287
"prices_send_n_prices": "{count,plural, =1{Send the price} other{Send {count} prices}}",

packages/smooth_app/lib/pages/crop_helper.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ abstract class CropHelper {
3737
/// Should we display the eraser with the crop grid?
3838
bool get enableEraser;
3939

40+
static Rect getLocalCropRectFromRect(final Rect crop) =>
41+
BackgroundTaskImage.getUpsizedRect(crop);
42+
4043
/// Returns the crop rect according to local cropping method * factor.
4144
@protected
4245
Rect getLocalCropRect(final CropController controller) =>
43-
BackgroundTaskImage.getUpsizedRect(controller.crop);
46+
getLocalCropRectFromRect(controller.crop);
4447

4548
@protected
4649
CropParameters getCropParameters({
@@ -55,14 +58,14 @@ abstract class CropHelper {
5558
fullFile: fullFile,
5659
smallCroppedFile: smallCroppedFile,
5760
rotation: controller.rotation.degrees,
58-
x1: cropRect.left.ceil(),
59-
y1: cropRect.top.ceil(),
60-
x2: cropRect.right.floor(),
61-
y2: cropRect.bottom.floor(),
61+
cropRect: cropRect,
6262
eraserCoordinates: eraserCoordinates,
6363
);
6464
}
6565

66+
/// Full-size crop, aka no crop.
67+
static const Rect fullImageCropRect = Rect.fromLTRB(0, 0, 1, 1);
68+
6669
static List<double> getEraserCoordinates(
6770
final List<Offset> offsets,
6871
) {

packages/smooth_app/lib/pages/crop_page.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@ class _CropPageState extends State<CropPage> {
105105

106106
Rect _getInitialRect() {
107107
if (widget.initialCropRect == null) {
108-
return const Rect.fromLTRB(0, 0, 1, 1);
108+
return CropHelper.fullImageCropRect;
109109
}
110110
// sometimes the server returns those crop values, meaning full photo.
111111
if (widget.initialCropRect!.left == -1 ||
112112
widget.initialCropRect!.top == -1 ||
113113
widget.initialCropRect!.right == -1 ||
114114
widget.initialCropRect!.bottom == -1) {
115-
return const Rect.fromLTRB(0, 0, 1, 1);
115+
return CropHelper.fullImageCropRect;
116116
}
117117
final Rect result;
118118
final CropRotation rotation = widget.initialRotation ?? CropRotation.up;

packages/smooth_app/lib/pages/crop_parameters.dart

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import 'dart:io';
2+
import 'dart:ui';
23

34
/// Parameters of the crop operation.
45
class CropParameters {
5-
const CropParameters({
6-
this.fullFile,
6+
CropParameters({
7+
required this.fullFile,
78
required this.smallCroppedFile,
89
required this.rotation,
9-
required this.x1,
10-
required this.y1,
11-
required this.x2,
12-
required this.y2,
10+
required Rect cropRect,
1311
this.eraserCoordinates,
14-
});
12+
}) : x1 = cropRect.left.ceil(),
13+
y1 = cropRect.top.ceil(),
14+
x2 = cropRect.right.floor(),
15+
y2 = cropRect.bottom.floor();
1516

1617
/// File of the full image.
1718
final File? fullFile;
1819

1920
/// File of the cropped image, resized according to the screen.
20-
final File smallCroppedFile;
21+
final File? smallCroppedFile;
2122

2223
final int rotation;
2324
final int x1;

packages/smooth_app/lib/pages/prices/eraser_model.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:crop_image/crop_image.dart';
22
import 'package:flutter/rendering.dart';
3+
import 'package:smooth_app/pages/crop_helper.dart';
34

45
/// Model about the eraser tool: coordinate computations.
56
class EraserModel {
@@ -33,6 +34,7 @@ class EraserModel {
3334
}
3435

3536
double get _deltaX => (_fullWidth - _imageWidth) / 2;
37+
3638
double get _deltaY => (_fullHeight - _imageHeight) / 2;
3739

3840
Offset? _latestStart;
@@ -42,11 +44,9 @@ class EraserModel {
4244

4345
int get length => offsets.length ~/ 2;
4446

45-
static const Rect _fullImageCropRect = Rect.fromLTRB(0, 0, 1, 1);
46-
4747
// From full image [0,1] to possibly cropped
4848
Offset _fromPct(final Offset offset) {
49-
final Rect rect = cropRect ?? _fullImageCropRect;
49+
final Rect rect = cropRect ?? CropHelper.fullImageCropRect;
5050
return switch (rotation) {
5151
CropRotation.down => Offset(
5252
(1 - offset.dx - rect.left) / rect.width * _imageWidth,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'dart:io';
2+
3+
import 'package:crop_image/crop_image.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
6+
import 'package:image_picker/image_picker.dart';
7+
import 'package:provider/provider.dart';
8+
import 'package:smooth_app/background/background_task_upload.dart';
9+
import 'package:smooth_app/database/dao_int.dart';
10+
import 'package:smooth_app/database/local_database.dart';
11+
import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart';
12+
import 'package:smooth_app/generic_lib/design_constants.dart';
13+
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
14+
import 'package:smooth_app/helpers/database_helper.dart';
15+
import 'package:smooth_app/pages/crop_helper.dart';
16+
import 'package:smooth_app/pages/crop_parameters.dart';
17+
import 'package:smooth_app/pages/prices/price_add_helper.dart';
18+
import 'package:smooth_app/pages/prices/price_model.dart';
19+
20+
/// Card that displays the bulk proof button for price adding.
21+
class PriceBulkProofCard extends StatefulWidget {
22+
const PriceBulkProofCard(this.formKey);
23+
24+
final GlobalKey<FormState> formKey;
25+
26+
@override
27+
State<PriceBulkProofCard> createState() => _PriceBulkProofCardState();
28+
}
29+
30+
class _PriceBulkProofCardState extends State<PriceBulkProofCard> {
31+
String _text = '';
32+
33+
@override
34+
Widget build(BuildContext context) {
35+
final PriceModel model = context.watch<PriceModel>();
36+
final AppLocalizations appLocalizations = AppLocalizations.of(context);
37+
return SmoothCardWithRoundedHeader(
38+
title: appLocalizations.prices_bulk_proof_upload_subtitle,
39+
leading: const Icon(Icons.document_scanner_rounded),
40+
contentPadding: const EdgeInsetsDirectional.symmetric(
41+
horizontal: SMALL_SPACE,
42+
vertical: MEDIUM_SPACE,
43+
),
44+
child: Column(
45+
children: <Widget>[
46+
ListTile(
47+
trailing: const Icon(Icons.warning),
48+
title: Text(
49+
appLocalizations.prices_bulk_proof_upload_warning,
50+
),
51+
),
52+
SmoothLargeButtonWithIcon(
53+
text: appLocalizations.prices_bulk_proof_upload_select,
54+
leadingIcon: const Icon(Icons.add),
55+
onPressed: model.location == null
56+
? null
57+
: () async => _selectAndUpload(model: model),
58+
),
59+
if (_text.isNotEmpty) Text(_text),
60+
],
61+
),
62+
);
63+
}
64+
65+
Future<void> _selectAndUpload({
66+
required PriceModel model,
67+
}) async {
68+
final PriceAddHelper priceAddHelper = PriceAddHelper(context);
69+
final LocalDatabase localDatabase = context.read<LocalDatabase>();
70+
const int imageQuality = 80;
71+
final Directory directory = await BackgroundTaskUpload.getDirectory();
72+
const String BULK_PROOF_IMAGE_SEQUENCE_KEY = 'bulk_proof_image_sequence';
73+
final Rect cropRect = CropHelper.getLocalCropRectFromRect(
74+
CropHelper.fullImageCropRect,
75+
);
76+
77+
setState(() => _text = '');
78+
final List<XFile> xFiles = await ImagePicker().pickMultiImage(
79+
imageQuality: imageQuality,
80+
requestFullMetadata: false,
81+
);
82+
if (xFiles.isEmpty) {
83+
return;
84+
}
85+
86+
if (!await priceAddHelper.acceptsWarning()) {
87+
return;
88+
}
89+
if (!mounted) {
90+
return;
91+
}
92+
late int index;
93+
final int count = xFiles.length;
94+
try {
95+
index = 0;
96+
for (final XFile xFile in xFiles) {
97+
index++;
98+
final int sequenceNumber = await getNextSequenceNumber(
99+
DaoInt(localDatabase),
100+
BULK_PROOF_IMAGE_SEQUENCE_KEY,
101+
);
102+
final String path = xFile.path;
103+
final File temporaryFile = File(path);
104+
final int pos = path.lastIndexOf(Platform.pathSeparator);
105+
final String filename = path.substring(pos + 1);
106+
final File toBeUploadedFile = File(
107+
'${directory.path}/bulk_proof_${sequenceNumber}_$filename',
108+
);
109+
setState(
110+
() => _text = 'Locally copying file #$index/$count',
111+
);
112+
await temporaryFile.copy(toBeUploadedFile.path);
113+
await temporaryFile.delete();
114+
115+
setState(() => _text = 'Preparing upload #$index/$count');
116+
model.cropParameters = CropParameters(
117+
fullFile: toBeUploadedFile,
118+
smallCroppedFile: null,
119+
rotation: CropRotation.up.degrees,
120+
cropRect: cropRect,
121+
eraserCoordinates: null,
122+
);
123+
if (!mounted) {
124+
return;
125+
}
126+
if (!await priceAddHelper.check(model, widget.formKey)) {
127+
return;
128+
}
129+
if (!mounted) {
130+
return;
131+
}
132+
await model.addTask(context);
133+
model.clearProof();
134+
}
135+
} catch (e) {
136+
setState(() => _text = 'Failed at image #$index/$count');
137+
return;
138+
}
139+
setState(() => _text = '');
140+
}
141+
}

packages/smooth_app/lib/pages/prices/price_proof_card.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class PriceProofCard extends StatelessWidget {
6161
builder: (BuildContext context, BoxConstraints constraints) =>
6262
Image(
6363
image: FileImage(
64-
File(model.cropParameters!.smallCroppedFile.path),
64+
File(model.cropParameters!.smallCroppedFile!.path),
6565
),
6666
width: constraints.maxWidth,
6767
height: constraints.maxWidth,

0 commit comments

Comments
 (0)