Skip to content
Open
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
15 changes: 13 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@
"@logIngredient": {},
"searchIngredient": "Search ingredient",
"@searchIngredient": {
"description": "Label on ingredient search form"
"description": "Label (or Hint text) on ingredient search form"
},
"nutritionalPlan": "Nutritional plan",
"@nutritionalPlan": {},
Expand All @@ -378,6 +378,11 @@
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"manageLists": "Manage lists",
"@manageLists": {
"description": "Label for the button to manage ingredient lists"
},
"clearSearchTerm": "Clear search term (text)",
"onlyLogging": "Only track calories",
"onlyLoggingHelpText": "Check the box if you only want to log your calories and don't want to setup a detailed nutritional plan with specific meals",
"goalMacro": "Macro goals",
Expand Down Expand Up @@ -494,7 +499,9 @@
"timeStartAhead": "Start time cannot be ahead of end time",
"@timeStartAhead": {},
"ingredient": "Ingredient",
"@ingredient": {},
"@ingredient": {
"description": "Title for the ingredients screen"
},
"energy": "Energy",
"@energy": {
"description": "Energy in a meal, ingredient etc. e.g. in kJ"
Expand Down Expand Up @@ -1282,6 +1289,10 @@
"@filterNutriscore": {
"description": "Heading for the Nutri-Score slider in the ingredient search filter dialog"
},
"nutriscoreValue": "Nutri-Score:",
"@filterNutriscore": {
"description": "label for Nutri-Score(nutriscore) for.eg, in subtitle of ingredient custom lists"
},
"filterNutriscoreOff": "Off",
"@filterNutriscoreOff": {
"description": "Label for the first slider stop, which disables the Nutri-Score filter"
Expand Down
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import 'package:wger/screens/form_screen.dart';
import 'package:wger/screens/gallery_screen.dart';
import 'package:wger/screens/gym_mode.dart';
import 'package:wger/screens/home_tabs_screen.dart';
import 'package:wger/screens/ingredient_detail_screen.dart';
import 'package:wger/screens/ingredients_screen.dart';
import 'package:wger/screens/log_meal_screen.dart';
import 'package:wger/screens/log_meals_screen.dart';
import 'package:wger/screens/measurement_categories_screen.dart';
Expand Down Expand Up @@ -200,6 +202,8 @@ class MainApp extends ConsumerWidget {
NutritionalPlansScreen.routeName: (ctx) => const NutritionalPlansScreen(),
NutritionalDiaryScreen.routeName: (ctx) => const NutritionalDiaryScreen(),
NutritionalPlanScreen.routeName: (ctx) => const NutritionalPlanScreen(),
IngredientDetailScreen.routeName: (ctx) => const IngredientDetailScreen(),
IngredientsScreen.routeName: (ctx) => const IngredientsScreen(),
LogMealsScreen.routeName: (ctx) => const LogMealsScreen(),
LogMealScreen.routeName: (ctx) => const LogMealScreen(),
WeightScreen.routeName: (ctx) => const WeightScreen(),
Expand Down
4 changes: 4 additions & 0 deletions lib/models/nutrition/ingredient_filters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ class IngredientFilters {
/// `nutriscore__lte`. `null` means the filter is off (slider at the
/// "No filter" position).
final NutriScore? nutriscoreMax;
final String searchTerm;

const IngredientFilters({
this.isVegan = false,
this.isVegetarian = false,
this.searchLanguage = SearchLanguage.currentAndEnglish,
this.nutriscoreMax,
this.searchTerm = '',
});

/// Locale-aware default filter set.
Expand All @@ -37,12 +39,14 @@ class IngredientFilters {
SearchLanguage? searchLanguage,
NutriScore? nutriscoreMax,
bool clearNutriscoreMax = false,
String? searchTerm,
}) {
return IngredientFilters(
isVegan: isVegan ?? this.isVegan,
isVegetarian: isVegetarian ?? this.isVegetarian,
searchLanguage: searchLanguage ?? this.searchLanguage,
nutriscoreMax: clearNutriscoreMax ? null : (nutriscoreMax ?? this.nutriscoreMax),
searchTerm: searchTerm ?? this.searchTerm,
);
}

Expand Down
26 changes: 26 additions & 0 deletions lib/providers/ingredient_filters_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,32 @@ class IngredientFiltersNotifier extends _$IngredientFiltersNotifier {
);
await PreferenceHelper.instance.saveIngredientNutriscoreMax(value);
}

/// Updates the search term in the state without persisting it.
void setSearchTerm(String value) {
final current = _current();
if (current.searchTerm != value) {
state = AsyncData(current.copyWith(searchTerm: value));
}
}

/// Resets all filters to their default values and clears persisted preferences.
Future<void> resetFilters() async {
state = const AsyncData(
IngredientFilters(
isVegan: false,
isVegetarian: false,
searchLanguage: SearchLanguage.current,
searchTerm: '',
),
);

final pref = PreferenceHelper.instance;
await pref.saveIngredientVeganFilter(false);
await pref.saveIngredientVegetarianFilter(false);
await pref.saveIngredientSearchLanguage(SearchLanguage.current);
await pref.saveIngredientNutriscoreMax(null);
}
}

// Synchronous helper provider to unwrap the AsyncValue produced by `ingredientFiltersProvider`
Expand Down
2 changes: 1 addition & 1 deletion lib/providers/ingredient_filters_notifier.g.dart

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

52 changes: 28 additions & 24 deletions lib/providers/ingredient_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@

import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/models/core/search_options.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/providers/ingredient_filters_notifier.dart';
import 'package:wger/providers/network_provider.dart';

import 'ingredient_repository.dart';

part 'ingredient_notifier.g.dart';

@riverpod
@Riverpod(keepAlive: true)
final class IngredientNotifier extends _$IngredientNotifier {
final _logger = Logger('IngredientProvider');

Expand All @@ -45,37 +47,30 @@ final class IngredientNotifier extends _$IngredientNotifier {
final Map<int, Future<Ingredient?>> _inflight = {};

@override
FutureOr<void> build() async {
_repo = ref.read(ingredientRepositoryProvider);
return null;
Stream<List<Ingredient>> build() {
ref.keepAlive();
_logger.finer('Building ingredient stream');
_repo = ref.watch(ingredientRepositoryProvider);

return _repo.watchAllDrift();
}

/// Fetch ingredient by [id] from the DB (one-shot) and update state.
/// If the id is present in the in-memory cache, the cached value is
/// returned immediately without a DB read. Simultaneous calls for the
/// same id are deduplicated. The provider `state` is used only as a
/// status signal (loading/done/error) and does not carry the Ingredient
/// itself — consumers should read the data from the notifier's cache.
/// same id are deduplicated.
Future<Ingredient?> fetch(int id) async {
_logger.finer('Fetching ingredient id: $id');

// Return cached result if present (could be null meaning 'not found').
if (_cache.containsKey(id)) {
final cached = _cache[id];
if (ref.mounted) {
state = const AsyncData(null);
}
return cached;
return _cache[id];
}

// If a fetch for this id is already ongoing, await it.
if (_inflight.containsKey(id)) {
_logger.finer('Awaiting in-flight fetch for id: $id');
final res = await _inflight[id];
if (ref.mounted) {
state = const AsyncData(null);
}
return res;
return await _inflight[id];
}

// Start a new DB read and store the future in _inflight to dedupe.
Expand All @@ -91,15 +86,8 @@ final class IngredientNotifier extends _$IngredientNotifier {
}

Future<Ingredient?> _doFetch(int id) async {
if (ref.mounted) {
state = const AsyncLoading();
}
final item = await _repo.getById(id);
_cache[id] = item; // store null as 'not found' as well
if (!ref.mounted) {
return item;
}
state = const AsyncData(null);
return item;
}

Expand Down Expand Up @@ -133,3 +121,19 @@ final class IngredientNotifier extends _$IngredientNotifier {
);
}
}

/// Trigger ingredient search flow by reacting to filter changes.
final searchedIngredientsProvider = FutureProvider<List<Ingredient>>((ref) async {
final filters = ref.watch(ingredientFiltersSyncProvider);

// search logic handles the Online/Offline routing
return ref
.read(ingredientProvider.notifier)
.searchIngredient(
filters.searchTerm,
searchLanguage: filters.searchLanguage,
isVegan: filters.isVegan,
isVegetarian: filters.isVegetarian,
nutriscoreMax: filters.nutriscoreMax,
);
});
17 changes: 9 additions & 8 deletions lib/providers/ingredient_notifier.g.dart

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

12 changes: 12 additions & 0 deletions lib/providers/ingredient_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ class IngredientRepository {
});
}

/// Watches all locally synced ingredients.
///
/// Only ingredients that are used in a nutritional plan or log are
/// synced to the device for offline storage in the Drift database.
Stream<List<Ingredient>> watchAllDrift() {
_logger.finer('Watching all synced ingredients');
final query = _baseJoinedQuery()..orderBy([OrderingTerm(expression: _db.ingredientTable.name)]);
return query.watch().map((rows) {
return _hydrate(rows);
});
}

/// Read a single ingredient by [id] once from the DB
Future<Ingredient?> getById(int id) async {
_logger.finer('Reading ingredient $id');
Expand Down
Loading