Skip to content

Commit

Permalink
feat: #3237 - improved gallery/camera choice (#3239)
Browse files Browse the repository at this point in the history
Impacted files:
* `app_en.arb`: added 2 related labels
* `app_fr.arb`: added 2 related labels
* `add_new_product_page.dart`: refactored the call to `startImageCropping`
* `confirm_and_upload_picture.dart`: refactored the call to `startImageCropping`; removed the code that also asked for the gallery/camera choice
* `edit_ingredients_page.dart`: refactored the call to `startImageCropping`
* `image_crop_page.dart`: simplified and upgraded methods `pickImageFile` and `startImageCropping`; added a method to select the best picture source
* `image_upload_card.dart`: refactored the call to `startImageCropping`
* `product_image_gallery_view.dart`: refactored the call to `startImageCropping`
* `user_preferences.dart`: added `enum`, getter, setter for the picture source
* `user_preferences_settings.dart`: added a choice for camera/gallery/select; fixed a possible overflow with "dark mode"
  • Loading branch information
monsieurtanuki authored Oct 31, 2022
1 parent 44350b5 commit cd288cd
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ class _ImageUploadCardState extends State<ImageUploadCard> {
ImageProvider? _imageFullProvider;

Future<void> _getImage() async {
final File? croppedImageFile =
await startImageCropping(context, showOptionDialog: true);
final File? croppedImageFile = await startImageCropping(this);

if (croppedImageFile != null) {
if (widget.productImageData.imageField != ImageField.OTHER) {
Expand Down
35 changes: 35 additions & 0 deletions packages/smooth_app/lib/data_models/user_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart';

/// User choice regarding the picture source.
enum UserPictureSource {
/// Always select between Gallery and Camera
SELECT('S'),

/// Always use Gallery
GALLERY('G'),

/// Always use Camera
CAMERA('C');

const UserPictureSource(this.tag);

final String tag;

static UserPictureSource get defaultValue => UserPictureSource.SELECT;

static UserPictureSource fromString(final String tag) =>
UserPictureSource.values
.firstWhere((final UserPictureSource source) => source.tag == tag);
}

class UserPreferences extends ChangeNotifier {
UserPreferences._shared(final SharedPreferences sharedPreferences)
: _sharedPreferences = sharedPreferences;
Expand Down Expand Up @@ -55,6 +77,9 @@ class UserPreferences extends ChangeNotifier {
/// Attribute group that is not collapsed
static const String _TAG_ACTIVE_ATTRIBUTE_GROUP = 'activeAttributeGroup';

/// User picture source
static const String _TAG_USER_PICTURE_SOURCE = 'userPictureSource';

Future<void> init(final ProductPreferences productPreferences) async {
if (_sharedPreferences.getBool(_TAG_INIT) != null) {
return;
Expand Down Expand Up @@ -229,4 +254,14 @@ class UserPreferences extends ChangeNotifier {
String get activeAttributeGroup =>
_sharedPreferences.getString(_TAG_ACTIVE_ATTRIBUTE_GROUP) ??
'nutritional_quality'; // TODO(monsieurtanuki): relatively safe but not nice to put a hard-coded value (even when highly probable)

UserPictureSource get userPictureSource => UserPictureSource.fromString(
_sharedPreferences.getString(_TAG_USER_PICTURE_SOURCE) ??
UserPictureSource.defaultValue.tag,
);

Future<void> setUserPictureSource(final UserPictureSource source) async {
await _sharedPreferences.setString(_TAG_USER_PICTURE_SOURCE, source.tag);
notifyListeners();
}
}
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,14 @@
"@image_edit_url_error": {
"description": "Error message, when editing image fails, due to missing url."
},
"user_picture_source_remember": "Remember my choice",
"@user_picture_source_remember": {
"description": "Checkbox label when select a picture source"
},
"user_picture_source_select": "Select each time",
"@user_picture_source_select": {
"description": "Choice of selecting the picture source each time"
},
"robotoff_continue": "Continue",
"@robotoff_continue": {
"description": "Shown when robotoff question are all answered and user wants to continue answering"
Expand Down
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,14 @@
"@image_edit_url_error": {
"description": "Error message, when editing image fails, due to missing url."
},
"user_picture_source_remember": "Se souvenir de mon choix",
"@user_picture_source_remember": {
"description": "Checkbox label when select a picture source"
},
"user_picture_source_select": "Choisir à chaque fois",
"@user_picture_source_select": {
"description": "Choice of selecting the picture source each time"
},
"robotoff_continue": "Continue",
"@robotoff_continue": {
"description": "Shown when robotoff question are all answered and user wants to continue answering"
Expand Down
110 changes: 71 additions & 39 deletions packages/smooth_app/lib/pages/image_crop_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/helpers/camera_helper.dart';
import 'package:smooth_app/pages/crop_helper.dart';

/// Crops an image from an existing file.
Expand Down Expand Up @@ -31,66 +34,95 @@ Future<File?> startImageCroppingNoPick(
}

/// Picks an image file from gallery or camera.
Future<XFile?> pickImageFile(
final BuildContext context, {
final bool showOptionDialog = false,
bool chooseFromGallery = false,
}) async {
if (showOptionDialog) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final bool? dialogFromGallery = await showDialog<bool>(
context: context,
builder: (BuildContext context) => SmoothAlertDialog(
Future<XFile?> pickImageFile(final State<StatefulWidget> widget) async {
final UserPictureSource? source = await _getUserPictureSource(widget.context);
if (source == null) {
return null;
}
final ImagePicker picker = ImagePicker();
if (source == UserPictureSource.GALLERY) {
return picker.pickImage(source: ImageSource.gallery);
}
return picker.pickImage(source: ImageSource.camera);
}

/// Returns the picture source selected by the user.
Future<UserPictureSource?> _getUserPictureSource(
final BuildContext context,
) async {
if (!CameraHelper.hasACamera) {
return UserPictureSource.GALLERY;
}
final UserPreferences userPreferences = context.read<UserPreferences>();
final UserPictureSource source = userPreferences.userPictureSource;
if (source != UserPictureSource.SELECT) {
return source;
}
final AppLocalizations appLocalizations = AppLocalizations.of(context);
bool? remember = false;
return showDialog<UserPictureSource>(
context: context,
builder: (BuildContext context) => StatefulBuilder(
builder: (
final BuildContext context,
final void Function(VoidCallback fn) setState,
) =>
SmoothAlertDialog(
title: appLocalizations.choose_image_source_title,
actionsAxis: Axis.vertical,
body: Text(appLocalizations.choose_image_source_body),
body: CheckboxListTile(
value: remember,
onChanged: (final bool? value) => setState(
() => remember = value,
),
title: Text(appLocalizations.user_picture_source_remember),
),
positiveAction: SmoothActionButton(
text: appLocalizations.settings_app_camera,
onPressed: () async => Navigator.pop(context, false),
onPressed: () {
const UserPictureSource result = UserPictureSource.CAMERA;
if (remember == true) {
userPreferences.setUserPictureSource(result);
}
Navigator.pop(context, result);
},
),
negativeAction: SmoothActionButton(
text: appLocalizations.gallery_source_label,
onPressed: () async => Navigator.pop(context, true),
onPressed: () {
const UserPictureSource result = UserPictureSource.GALLERY;
if (remember == true) {
userPreferences.setUserPictureSource(result);
}
Navigator.pop(context, result);
},
),
),
);
if (dialogFromGallery == null) {
return null;
}
chooseFromGallery = dialogFromGallery;
}
final ImagePicker picker = ImagePicker();
if (chooseFromGallery) {
return picker.pickImage(source: ImageSource.gallery);
}
return picker.pickImage(source: ImageSource.camera);
),
);
}

/// Crops an image picked from the gallery or camera.
Future<File?> startImageCropping(
BuildContext context, {
bool showOptionDialog = false,
bool chooseFromGallery = false,
}) async {
Future<File?> startImageCropping(final State<StatefulWidget> widget) async {
// Show a loading page on the Flutter side
final NavigatorState navigator = Navigator.of(context);
final CropHelper cropHelper = CropHelper.getCurrent(context);
final NavigatorState navigator = Navigator.of(widget.context);
final CropHelper cropHelper = CropHelper.getCurrent(widget.context);
await _showScreenBetween(navigator);

// ignore: use_build_context_synchronously
final XFile? pickedXFile = await pickImageFile(
context,
chooseFromGallery: chooseFromGallery,
showOptionDialog: showOptionDialog,
);
if (!widget.mounted) {
return null;
}
final XFile? pickedXFile = await pickImageFile(widget);
if (pickedXFile == null) {
await _hideScreenBetween(navigator);
return null;
}

// ignore: use_build_context_synchronously
if (!widget.mounted) {
return null;
}
final String? croppedPath = await cropHelper.getCroppedPath(
context,
widget.context,
pickedXFile.path,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,28 @@ class _ApplicationSettings extends StatelessWidget {
label: appLocalizations.settings_app_app,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: LARGE_SPACE,
vertical: MEDIUM_SPACE,
padding: const EdgeInsets.only(
left: LARGE_SPACE,
top: MEDIUM_SPACE,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
appLocalizations.darkmode,
style: themeData.textTheme.headline4,
),
],
),
),
Padding(
padding: const EdgeInsets.only(
right: LARGE_SPACE,
bottom: MEDIUM_SPACE,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
DropdownButton<String>(
value: themeProvider.currentTheme,
elevation: 16,
Expand Down Expand Up @@ -122,6 +133,53 @@ class _ApplicationSettings extends StatelessWidget {
),
minVerticalPadding: MEDIUM_SPACE,
),
const UserPreferencesListItemDivider(),
Padding(
padding: const EdgeInsets.only(
left: LARGE_SPACE,
top: MEDIUM_SPACE,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
appLocalizations.choose_image_source_title,
style: themeData.textTheme.headline4,
),
],
),
),
Padding(
padding: const EdgeInsets.only(
right: LARGE_SPACE,
bottom: MEDIUM_SPACE,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
DropdownButton<UserPictureSource>(
value: userPreferences.userPictureSource,
elevation: 16,
onChanged: (final UserPictureSource? newValue) async =>
userPreferences.setUserPictureSource(newValue!),
items: <DropdownMenuItem<UserPictureSource>>[
DropdownMenuItem<UserPictureSource>(
value: UserPictureSource.SELECT,
child: Text(appLocalizations.user_picture_source_select),
),
DropdownMenuItem<UserPictureSource>(
value: UserPictureSource.CAMERA,
child: Text(appLocalizations.settings_app_camera),
),
DropdownMenuItem<UserPictureSource>(
value: UserPictureSource.GALLERY,
child: Text(appLocalizations.gallery_source_label),
)
],
),
],
),
),
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class _AddNewProductPageState extends State<AddNewProductPage> {
text: _getAddPhotoButtonText(context, imageType),
icon: Icons.camera_alt,
onPressed: () async {
final File? initialPhoto = await startImageCropping(context);
final File? initialPhoto = await startImageCropping(this);
if (initialPhoto == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class _ConfirmAndUploadPictureState extends State<ConfirmAndUploadPicture> {
alignment: WrapAlignment.center,
children: <Widget>[
OutlinedButton.icon(
icon: const Icon(Icons.camera),
icon: const Icon(Icons.camera_alt),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
themeData.colorScheme.background,
Expand All @@ -79,8 +79,7 @@ class _ConfirmAndUploadPictureState extends State<ConfirmAndUploadPicture> {
),
),
onPressed: () async {
retakenPhoto = await startImageCropping(context,
chooseFromGallery: false);
retakenPhoto = await startImageCropping(this);
if (retakenPhoto == null) {
if (!mounted) {
return;
Expand All @@ -97,37 +96,6 @@ class _ConfirmAndUploadPictureState extends State<ConfirmAndUploadPicture> {
},
label: Text(appLocalizations.capture),
),
OutlinedButton.icon(
icon: const Icon(Icons.photo_sharp),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
themeData.colorScheme.background,
),
shape: MaterialStateProperty.all(
const RoundedRectangleBorder(
borderRadius: ROUNDED_BORDER_RADIUS,
),
),
),
onPressed: () async {
retakenPhoto = await startImageCropping(context,
chooseFromGallery: true);
if (retakenPhoto == null) {
if (!mounted) {
return;
}
// User chose not to upload the image.
Navigator.pop(context);
return;
}
setState(
() {
photo = retakenPhoto!;
},
);
},
label: Text(appLocalizations.choose_from_gallery),
),
OutlinedButton.icon(
icon: const Icon(Icons.edit),
style: ButtonStyle(
Expand Down
Loading

0 comments on commit cd288cd

Please sign in to comment.