Skip to content

Commit

Permalink
feat: 2427 - added "unselect image" button (#3618)
Browse files Browse the repository at this point in the history
New files:
* `background_task_unselect.dart`: Background task about unselecting a product image.
* `edit_image_button.dart`: Standard text button for the "edit image" pages. Used to be in `NewCropPage` as `_OutlinedButton`.

Impacted files:
* `abstract_background_task.dart`: added new "unselect" task
* `app_en.arb`: added a label for the "unselect image" button
* `background_task_manager.dart`: unrelated minor fix
* `new_crop_page.dart`: moved `_OutlinedButton` code to new file `edit_image_button.dart`
* `offline_tasks_page.dart`: moved code to `OperationType`
* `operation_type.dart`: new `enum` for "unselect"; new method `getLabel`
* `product_image_viewer.dart`: changed the layout in order to add the "unselect image" button
* `up_to_date_changes.dart`: included the new "unselect" type to product changes
  • Loading branch information
monsieurtanuki committed Jan 23, 2023
1 parent 68f7d86 commit 153c53a
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:smooth_app/background/background_task_details.dart';
import 'package:smooth_app/background/background_task_image.dart';
import 'package:smooth_app/background/background_task_manager.dart';
import 'package:smooth_app/background/background_task_refresh_later.dart';
import 'package:smooth_app/background/background_task_unselect.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/pages/product/common/product_refresher.dart';
Expand Down Expand Up @@ -45,6 +46,7 @@ abstract class AbstractBackgroundTask {
static AbstractBackgroundTask? fromJson(final Map<String, dynamic> map) =>
BackgroundTaskDetails.fromJson(map) ??
BackgroundTaskImage.fromJson(map) ??
BackgroundTaskUnselect.fromJson(map) ??
BackgroundTaskRefreshLater.fromJson(map);

/// Executes the background task: upload, download, update locally.
Expand Down
10 changes: 6 additions & 4 deletions packages/smooth_app/lib/background/background_task_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,20 @@ class BackgroundTaskManager {
_debugPrint('get all tasks/0');
final List<AbstractBackgroundTask> result = <AbstractBackgroundTask>[];
final List<String> list = localDatabase.getAllTaskIds();
final List<String> duplicateTaskIds = <String>[];
final List<String> removeTaskIds = <String>[];
if (list.isEmpty) {
return result;
}
for (final String taskId in list) {
final AbstractBackgroundTask? task = _get(taskId);
if (task == null) {
// unexpected, but let's remove that null task anyway.
await _finishTask(taskId);
_debugPrint('get all tasks/unexpected/$taskId');
removeTaskIds.add(taskId);
continue;
}
if (!task.mayRunNow()) {
_debugPrint('get all tasks/maynotrun/$taskId');
// let's ignore this task: it's not supposed to be run now.
continue;
}
Expand All @@ -248,7 +250,7 @@ class BackgroundTaskManager {
if (result[i].stamp == stamp) {
final String removeTaskId = result[i].uniqueId;
_debugPrint('duplicate stamp, task $removeTaskId being removed...');
duplicateTaskIds.add(removeTaskId);
removeTaskIds.add(removeTaskId);
removeMe = i;
break;
}
Expand All @@ -261,7 +263,7 @@ class BackgroundTaskManager {
}
result.add(task);
}
for (final String taskId in duplicateTaskIds) {
for (final String taskId in removeTaskIds) {
await _finishTask(taskId);
}
_debugPrint('get all tasks returned (begin)');
Expand Down
175 changes: 175 additions & 0 deletions packages/smooth_app/lib/background/background_task_unselect.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/background/abstract_background_task.dart';
import 'package:smooth_app/background/background_task_image.dart';
import 'package:smooth_app/background/background_task_refresh_later.dart';
import 'package:smooth_app/data_models/operation_type.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/query/product_query.dart';

/// Background task about unselecting a product image.
class BackgroundTaskUnselect extends AbstractBackgroundTask {
const BackgroundTaskUnselect._({
required super.processName,
required super.uniqueId,
required super.barcode,
required super.languageCode,
required super.user,
required super.country,
required super.stamp,
required this.imageField,
});

BackgroundTaskUnselect._fromJson(Map<String, dynamic> json)
: this._(
processName: json['processName'] as String,
uniqueId: json['uniqueId'] as String,
barcode: json['barcode'] as String,
languageCode: json['languageCode'] as String,
user: json['user'] as String,
country: json['country'] as String,
imageField: json['imageField'] as String,
stamp: json['stamp'] as String,
);

/// Task ID.
static const String _PROCESS_NAME = 'IMAGE_UNSELECT';

static const OperationType _operationType = OperationType.unselect;

final String imageField;

@override
Map<String, dynamic> toJson() => <String, dynamic>{
'processName': processName,
'uniqueId': uniqueId,
'barcode': barcode,
'languageCode': languageCode,
'user': user,
'country': country,
'imageField': imageField,
'stamp': stamp,
};

/// Returns the deserialized background task if possible, or null.
static AbstractBackgroundTask? fromJson(final Map<String, dynamic> map) {
try {
final AbstractBackgroundTask result =
BackgroundTaskUnselect._fromJson(map);
if (result.processName == _PROCESS_NAME) {
return result;
}
} catch (e) {
//
}
return null;
}

/// Adds the background task about unselecting a product image.
static Future<void> addTask(
final String barcode, {
required final ImageField imageField,
required final State<StatefulWidget> widget,
}) async {
final LocalDatabase localDatabase = widget.context.read<LocalDatabase>();
final String uniqueId = await _operationType.getNewKey(
localDatabase,
barcode,
);
final AbstractBackgroundTask task = _getNewTask(
barcode,
imageField,
uniqueId,
);
await task.addToManager(localDatabase, widget: widget);
}

@override
String? getSnackBarMessage(final AppLocalizations appLocalizations) =>
appLocalizations.product_task_background_schedule;

/// Returns a new background task about unselecting a product image.
static BackgroundTaskUnselect _getNewTask(
final String barcode,
final ImageField imageField,
final String uniqueId,
) =>
BackgroundTaskUnselect._(
uniqueId: uniqueId,
barcode: barcode,
processName: _PROCESS_NAME,
imageField: imageField.offTag,
languageCode: ProductQuery.getLanguage().code,
user: jsonEncode(ProductQuery.getUser().toJson()),
country: ProductQuery.getCountry()!.offTag,
// same stamp as image upload
stamp: BackgroundTaskImage.getStamp(
barcode,
imageField.offTag,
ProductQuery.getLanguage().code,
),
);

@override
Future<void> preExecute(final LocalDatabase localDatabase) async =>
localDatabase.upToDate.addChange(uniqueId, _getUnselectedProduct());

@override
Future<void> postExecute(
final LocalDatabase localDatabase,
final bool success,
) async {
localDatabase.upToDate.terminate(uniqueId);
localDatabase.notifyListeners();
if (success) {
await BackgroundTaskRefreshLater.addTask(
barcode,
localDatabase: localDatabase,
);
}
}

/// Unselects the product image.
@override
Future<void> upload() async => OpenFoodAPIClient.unselectProductImage(
barcode: barcode,
imageField: ImageField.fromOffTag(imageField)!,
language: getLanguage(),
user: getUser(),
);

/// Returns a product with "unselected" image.
///
/// The problem is that `null` may mean both
/// * "I don't change this value"
/// * "I change the value to null"
/// Here we put an empty string instead, to be understood as "force to null!".
Product _getUnselectedProduct() {
final Product result = Product(barcode: barcode);
switch (ImageField.fromOffTag(imageField)!) {
case ImageField.FRONT:
result.imageFrontUrl = '';
result.imageFrontSmallUrl = '';
break;
case ImageField.INGREDIENTS:
result.imageIngredientsUrl = '';
result.imageIngredientsSmallUrl = '';
break;
case ImageField.NUTRITION:
result.imageNutritionUrl = '';
result.imageNutritionSmallUrl = '';
break;
case ImageField.PACKAGING:
result.imagePackagingUrl = '';
result.imagePackagingSmallUrl = '';
break;
case ImageField.OTHER:
// We do nothing. Actually we're not supposed to unselect other images.
}
return result;
}
}
15 changes: 15 additions & 0 deletions packages/smooth_app/lib/data_models/operation_type.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:smooth_app/database/dao_int.dart';
import 'package:smooth_app/database/dao_transient_operation.dart';
import 'package:smooth_app/database/local_database.dart';
Expand All @@ -12,6 +13,7 @@ import 'package:smooth_app/helpers/database_helper.dart';
/// * possibly, which barcode (not useful yet)
enum OperationType {
image('I'),
unselect('U'),
refreshLater('R'),
details('D');

Expand All @@ -37,6 +39,19 @@ enum OperationType {
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.image:
return appLocalizations.background_task_operation_image;
case OperationType.unselect:
return 'Unselect a product image';
case OperationType.refreshLater:
return 'Waiting 10 min before refreshing product to get all automatic edits';
}
}

static int getSequentialId(final TransientOperation operation) {
final List<String> keyItems =
operation.key.split(_transientHeaderSeparator);
Expand Down
42 changes: 30 additions & 12 deletions packages/smooth_app/lib/data_models/up_to_date_changes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ class UpToDateChanges {
/// For a barcode, map of the actions (pending and done).
final DaoTransientOperation _daoTransientProduct;

OperationType get taskActionable => OperationType.details;

/// Returns all the actions related to a barcode, sorted by id.
Iterable<TransientOperation> getSortedOperations(final String barcode) {
final List<TransientOperation> result = <TransientOperation>[];
for (final TransientOperation transientProduct
in _daoTransientProduct.getAll(barcode)) {
if (taskActionable.matches(transientProduct)) {
if (OperationType.details.matches(transientProduct) ||
OperationType.unselect.matches(transientProduct)) {
result.add(transientProduct);
}
}
Expand Down Expand Up @@ -57,7 +56,6 @@ class UpToDateChanges {
/// Currently limited to the fields modified by
/// * [BackgroundTaskDetails]
/// * [BackgroundTaskImage]
// TODO(monsieurtanuki): refactor this "à la copyWith" or something like that
Product _overwrite(final Product initial, final Product change) {
if (change.productName != null) {
initial.productName = change.productName;
Expand Down Expand Up @@ -118,29 +116,49 @@ class UpToDateChanges {
if (change.countriesTagsInLanguages != null) {
initial.countriesTagsInLanguages = change.countriesTagsInLanguages;
}

/// In some cases we want to force null; we do that with an empty String.
String? emptyMeansNull(final String value) => value.isEmpty ? null : value;

if (change.imageFrontUrl != null) {
initial.imageFrontUrl = change.imageFrontUrl;
initial.imageFrontUrl = emptyMeansNull(
change.imageFrontUrl!,
);
}
if (change.imageFrontSmallUrl != null) {
initial.imageFrontSmallUrl = change.imageFrontSmallUrl;
initial.imageFrontSmallUrl = emptyMeansNull(
change.imageFrontSmallUrl!,
);
}
if (change.imageIngredientsUrl != null) {
initial.imageIngredientsUrl = change.imageIngredientsUrl;
initial.imageIngredientsUrl = emptyMeansNull(
change.imageIngredientsUrl!,
);
}
if (change.imageIngredientsSmallUrl != null) {
initial.imageIngredientsSmallUrl = change.imageIngredientsSmallUrl;
initial.imageIngredientsSmallUrl = emptyMeansNull(
change.imageIngredientsSmallUrl!,
);
}
if (change.imageNutritionUrl != null) {
initial.imageNutritionUrl = change.imageNutritionUrl;
initial.imageNutritionUrl = emptyMeansNull(
change.imageNutritionUrl!,
);
}
if (change.imageNutritionSmallUrl != null) {
initial.imageNutritionSmallUrl = change.imageNutritionSmallUrl;
initial.imageNutritionSmallUrl = emptyMeansNull(
change.imageNutritionSmallUrl!,
);
}
if (change.imagePackagingUrl != null) {
initial.imagePackagingUrl = change.imagePackagingUrl;
initial.imagePackagingUrl = emptyMeansNull(
change.imagePackagingUrl!,
);
}
if (change.imagePackagingSmallUrl != null) {
initial.imagePackagingSmallUrl = change.imagePackagingSmallUrl;
initial.imagePackagingSmallUrl = emptyMeansNull(
change.imagePackagingSmallUrl!,
);
}
if (change.website != null) {
initial.website = change.website;
Expand Down
4 changes: 4 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,10 @@
"@edit_photo_button_label": {
"description": "Edit photo button label"
},
"edit_photo_unselect_button_label": "Unselect photo",
"@edit_photo_unselect_button_label": {
"description": "Edit 'unselect photo' button label"
},
"category_picker_screen_title": "Categories",
"@category_picker_screen_title": {
"description": "Categories picker screen title"
Expand Down
23 changes: 3 additions & 20 deletions packages/smooth_app/lib/pages/offline_tasks_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ class _OfflineTaskState extends State<OfflineTaskPage> {
},
title: Text(
'${OperationType.getBarcode(taskId)}'
' (${_getOperationLabel(
OperationType.getOperationType(taskId),
appLocalizations,
)})',
' (${OperationType.getOperationType(taskId)?.getLabel(
appLocalizations,
) ?? appLocalizations.background_task_operation_unknown})',
),
subtitle: Text(_getMessage(status, appLocalizations)),
trailing: const Icon(Icons.clear),
Expand All @@ -76,22 +75,6 @@ class _OfflineTaskState extends State<OfflineTaskPage> {
);
}

String _getOperationLabel(
final OperationType? type,
final AppLocalizations appLocalizations,
) {
switch (type) {
case null:
return appLocalizations.background_task_operation_unknown;
case OperationType.details:
return appLocalizations.background_task_operation_details;
case OperationType.image:
return appLocalizations.background_task_operation_image;
case OperationType.refreshLater:
return 'Waiting 10 min before refreshing product to get all automatic edits';
}
}

String _getMessage(
final String? status,
final AppLocalizations appLocalizations,
Expand Down

0 comments on commit 153c53a

Please sign in to comment.