Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions lib/models/nutrition/ingredient.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class Ingredient {
@JsonKey(required: true)
final String name;

/// Brand of the product
@JsonKey(name: 'brand')
final String? brand;

@JsonKey(required: true, name: 'created')
final DateTime created;

Expand Down Expand Up @@ -123,6 +127,7 @@ class Ingredient {
required this.id,
required this.code,
required this.name,
this.brand,
required this.created,
required this.energy,
required this.carbohydrates,
Expand Down
2 changes: 2 additions & 0 deletions lib/models/nutrition/ingredient.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion lib/widgets/nutrition/ingredient_dialogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,22 @@ class IngredientDetails extends StatelessWidget {
}

return AlertDialog(
title: (snapshot.hasData) ? Text(ingredient!.name) : null,
title: snapshot.hasData
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(ingredient!.name),
if (ingredient.brand != null && ingredient.brand!.isNotEmpty)
Text(
ingredient.brand!,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
)
: null,
content: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
Expand Down
17 changes: 15 additions & 2 deletions lib/widgets/nutrition/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,21 @@ class _IngredientTypeaheadState extends ConsumerState<IngredientTypeahead> {
: const CircleIconAvatar(
Icon(Icons.image, color: Colors.grey),
),
title: Text(
ingredient.name,
title: Text.rich(
Copy link
Copy Markdown
Member

@rolandgeider rolandgeider May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you added this in the title? Unless it breaks the UI (which I didn't test), I'd vote to simply show the brand in subtitle

Nevermind, we already have a subtitle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yeah, I saw the subtitle is used for the chips/tags, and I thought a while about what the best design choice is of where to put the brand, but adding it to the subtitle felt too crowded and also mismatched with the chips, and I didn't want to add a 'third' row as it would just increase the per-item space too much. Since most items tend to have a short(er) name, I think name and brand sharing the title space looked okay. I'm open to suggestions though!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, you are right, the name and the brand belong more together.

If you want to fix a small thing, instead of changing the alpha here, use the color from one of the Theme.of(context).colorScheme.xyz (I think onSurfaceVariant?), then this also automatically works on the dark mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I fixed it with the latest commit. I decided to still modify the alpha channel a little in the ListTile title, because otherwise I felt like there was too little contrast between the ingredient name and the brand (since they are sharing a line). For the IngredientDetails view, I use onSurfaceVariant unmodified, since it's positioned in the line below and has smaller fontSize anyway.

TextSpan(
children: [
TextSpan(text: ingredient.name),
if (ingredient.brand != null && ingredient.brand!.isNotEmpty)
TextSpan(
text: ' ${ingredient.brand}',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Expand Down
21 changes: 21 additions & 0 deletions test/nutrition/ingredient_typeahead_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,27 @@ void main() {
expect(find.text('Vegan'), findsNothing);
});

testWidgets('Shows brand inline in search result tile', (WidgetTester tester) async {
// ingredient3 (Broccoli cake) has brand 'Weightwatchers'
when(
mockNutrition.searchIngredient(
any,
languageCode: anyNamed('languageCode'),
searchLanguage: anyNamed('searchLanguage'),
isVegan: anyNamed('isVegan'),
isVegetarian: anyNamed('isVegetarian'),
nutriscoreMax: anyNamed('nutriscoreMax'),
),
).thenAnswer((_) => Future.value([ingredient3]));

await tester.pumpWidget(createWidgetUnderTest());
await tester.enterText(find.byType(TextFormField), 'Broccoli');
await tester.pump(const Duration(milliseconds: 600));
await tester.pumpAndSettle();

expect(find.textContaining('Weightwatchers'), findsOneWidget);
});

testWidgets('Shows no dietary chips when ingredient has no info', (WidgetTester tester) async {
// ingredient2 (Burger soup) has no dietary info
when(
Expand Down
61 changes: 61 additions & 0 deletions test/widgets/nutrition/ingredient_dialogs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,34 @@ import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/widgets/nutrition/ingredient_dialogs.dart';

import '../../../test_data/nutritional_plans.dart';

Future<void> pumpIngredientDetailsDialog(
WidgetTester tester, {
required AsyncSnapshot<Ingredient> snapshot,
}) async {
await tester.pumpWidget(
MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (_) => IngredientDetails(snapshot),
);
},
child: const Text('Show Dialog'),
),
),
),
),
);
}

Future<void> pumpIngredientScanDialog(
WidgetTester tester, {
required AsyncSnapshot<Ingredient?> snapshot,
Expand Down Expand Up @@ -54,6 +82,39 @@ Future<void> pumpIngredientScanDialog(
}

void main() {
group('IngredientDetails tests', () {
testWidgets('shows brand below name in title when ingredient has a brand', (
WidgetTester tester,
) async {
// ingredient3 (Broccoli cake, brand: 'Weightwatchers')
final snapshot = AsyncSnapshot<Ingredient>.withData(ConnectionState.done, ingredient3);

await pumpIngredientDetailsDialog(tester, snapshot: snapshot);
await tester.tap(find.text('Show Dialog'));
await tester.pumpAndSettle();

expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text('Broccoli cake'), findsOneWidget);
expect(find.text('Weightwatchers'), findsOneWidget);
});

testWidgets('does not show brand in title when ingredient has no brand', (
WidgetTester tester,
) async {
// ingredient1 (Water, brand: null)
final snapshot = AsyncSnapshot<Ingredient>.withData(ConnectionState.done, ingredient1);

await pumpIngredientDetailsDialog(tester, snapshot: snapshot);
await tester.tap(find.text('Show Dialog'));
await tester.pumpAndSettle();

expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text('Water'), findsOneWidget);
// brand: null must not render as the literal string "null"
expect(find.text('null'), findsNothing);
});
});

group('IngredientScanResultDialog tests', () {
const testBarcode = '1234567890123';

Expand Down
5 changes: 5 additions & 0 deletions test_data/nutritional_plans.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final ingredient1 = Ingredient(
id: 1,
code: '123456787',
name: 'Water',
brand: null,
created: DateTime(2021, 5, 1),
energy: 500,
carbohydrates: 10,
Expand All @@ -52,6 +53,7 @@ final ingredient2 = Ingredient(
id: 2,
code: '123456788',
name: 'Burger soup',
brand: null,
created: DateTime(2021, 5, 10),
energy: 25,
carbohydrates: 10,
Expand All @@ -69,6 +71,7 @@ final ingredient3 = Ingredient(
id: 3,
code: '123456789',
name: 'Broccoli cake',
brand: 'Weightwatchers',
created: DateTime(2021, 5, 2),
energy: 1200,
carbohydrates: 110,
Expand All @@ -86,6 +89,7 @@ final muesli = Ingredient(
id: 1,
code: '123456787',
name: 'Müsli',
brand: 'Spar Gourmet',
created: DateTime(2021, 5, 1),
energy: 500,
carbohydrates: 10,
Expand All @@ -106,6 +110,7 @@ final milk = Ingredient(
id: 1,
code: '123456787',
name: 'Milk',
brand: null,
created: DateTime(2021, 5, 1),
energy: 500,
carbohydrates: 10,
Expand Down