diff --git a/app/lib/features/bangs/domain/providers/bangs.dart b/app/lib/features/bangs/domain/providers/bangs.dart index 197cc04e..fd2b9d5b 100644 --- a/app/lib/features/bangs/domain/providers/bangs.dart +++ b/app/lib/features/bangs/domain/providers/bangs.dart @@ -81,7 +81,12 @@ Stream> frequentBangList(Ref ref) { @Riverpod() Stream> searchHistory(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); - return repository.watchSearchHistory(limit: 3); //TODO: make count dynamic + final maxSearchHistoryEntries = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.maxSearchHistoryEntries, + ), + ); + return repository.watchSearchHistory(limit: maxSearchHistoryEntries); } @Riverpod() diff --git a/app/lib/features/bangs/domain/providers/bangs.g.dart b/app/lib/features/bangs/domain/providers/bangs.g.dart index 7a15b6b2..0b8d59c8 100644 --- a/app/lib/features/bangs/domain/providers/bangs.g.dart +++ b/app/lib/features/bangs/domain/providers/bangs.g.dart @@ -354,7 +354,7 @@ final class SearchHistoryProvider } } -String _$searchHistoryHash() => r'7b97798729643de8e44fd8024cdc02f35124b08b'; +String _$searchHistoryHash() => r'5f9508a6b286bfcd1b641bd429de46ad052a3dfe'; @ProviderFor(lastSyncOfGroup) final lastSyncOfGroupProvider = LastSyncOfGroupFamily._(); diff --git a/app/lib/features/bangs/domain/providers/search.dart b/app/lib/features/bangs/domain/providers/search.dart index c6de6891..6c99ec7a 100644 --- a/app/lib/features/bangs/domain/providers/search.dart +++ b/app/lib/features/bangs/domain/providers/search.dart @@ -24,6 +24,7 @@ import 'package:weblibre/features/bangs/data/models/bang_data.dart'; import 'package:weblibre/features/bangs/data/providers.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/bangs/domain/repositories/data.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; part 'search.g.dart'; @@ -33,14 +34,15 @@ class BangSearch extends _$BangSearch { Future triggerBangSearch(BangData bang, String searchQuery) async { final bangDataNotifier = ref.read(bangDataRepositoryProvider.notifier); + final settings = ref.read(generalSettingsWithDefaultsProvider); await bangDataNotifier.increaseFrequency(bang.toKey()); await bangDataNotifier.addSearchEntry( bang.group, bang.trigger, searchQuery, - maxEntryCount: 3, - ); //TODO: make count dynamic + maxEntryCount: settings.maxSearchHistoryEntries, + ); return bang.getTemplateUrl(searchQuery); } diff --git a/app/lib/features/bangs/domain/providers/search.g.dart b/app/lib/features/bangs/domain/providers/search.g.dart index efacf7fb..7c611838 100644 --- a/app/lib/features/bangs/domain/providers/search.g.dart +++ b/app/lib/features/bangs/domain/providers/search.g.dart @@ -33,7 +33,7 @@ final class BangSearchProvider BangSearch create() => BangSearch(); } -String _$bangSearchHash() => r'cd8c6ec433a61979e5feb09014739be92180513b'; +String _$bangSearchHash() => r'7993bba3765d24ca9a7a17eca86ffdbc7c1b5e65'; abstract class _$BangSearch extends $StreamNotifier> { Stream> build(); diff --git a/app/lib/features/bangs/domain/repositories/data.dart b/app/lib/features/bangs/domain/repositories/data.dart index 77cb6f7a..2983e128 100644 --- a/app/lib/features/bangs/domain/repositories/data.dart +++ b/app/lib/features/bangs/domain/repositories/data.dart @@ -115,7 +115,12 @@ class BangDataRepository extends _$BangDataRepository { String trigger, String searchQuery, { required int maxEntryCount, - }) { + }) async { + // Skip capturing history if maxEntryCount is 0 + if (maxEntryCount <= 0) { + return; + } + final db = ref.read(bangDatabaseProvider); //Pack in a transaction to bundle rebuilds of watch() queries return db.transaction(() async { diff --git a/app/lib/features/bangs/domain/repositories/data.g.dart b/app/lib/features/bangs/domain/repositories/data.g.dart index 9b9fa613..e7a85edb 100644 --- a/app/lib/features/bangs/domain/repositories/data.g.dart +++ b/app/lib/features/bangs/domain/repositories/data.g.dart @@ -42,7 +42,7 @@ final class BangDataRepositoryProvider } String _$bangDataRepositoryHash() => - r'e3833d71ca51bfe51feb9f137e154dfebf8b053d'; + r'c562ef10d75ca6dee13805f84d2491cf2a89aaac'; abstract class _$BangDataRepository extends $Notifier { void build(); diff --git a/app/lib/features/bangs/domain/services/search_history_cleanup.dart b/app/lib/features/bangs/domain/services/search_history_cleanup.dart new file mode 100644 index 00000000..fd574d11 --- /dev/null +++ b/app/lib/features/bangs/domain/services/search_history_cleanup.dart @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/bangs/data/providers.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; + +part 'search_history_cleanup.g.dart'; + +/// Service that listens to maxSearchHistoryEntries setting changes +/// and cleans up search history when the limit is reduced. +@Riverpod(keepAlive: true) +class SearchHistoryCleanupService extends _$SearchHistoryCleanupService { + @override + void build() { + ref.listen( + generalSettingsWithDefaultsProvider.select( + (settings) => settings.maxSearchHistoryEntries, + ), + (previous, next) async { + // Only cleanup when limit is reduced (including to 0) + if (previous != null && next < previous) { + final db = ref.read(bangDatabaseProvider); + await db.definitionsDrift.evictHistoryEntries(limit: next); + } + }, + ); + } +} diff --git a/app/lib/features/bangs/domain/services/search_history_cleanup.g.dart b/app/lib/features/bangs/domain/services/search_history_cleanup.g.dart new file mode 100644 index 00000000..5b1e9569 --- /dev/null +++ b/app/lib/features/bangs/domain/services/search_history_cleanup.g.dart @@ -0,0 +1,73 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'search_history_cleanup.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Service that listens to maxSearchHistoryEntries setting changes +/// and cleans up search history when the limit is reduced. + +@ProviderFor(SearchHistoryCleanupService) +final searchHistoryCleanupServiceProvider = + SearchHistoryCleanupServiceProvider._(); + +/// Service that listens to maxSearchHistoryEntries setting changes +/// and cleans up search history when the limit is reduced. +final class SearchHistoryCleanupServiceProvider + extends $NotifierProvider { + /// Service that listens to maxSearchHistoryEntries setting changes + /// and cleans up search history when the limit is reduced. + SearchHistoryCleanupServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'searchHistoryCleanupServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$searchHistoryCleanupServiceHash(); + + @$internal + @override + SearchHistoryCleanupService create() => SearchHistoryCleanupService(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$searchHistoryCleanupServiceHash() => + r'4e84ab50fa2ad55c615bb63a667c95383715570b'; + +/// Service that listens to maxSearchHistoryEntries setting changes +/// and cleans up search history when the limit is reduced. + +abstract class _$SearchHistoryCleanupService extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index 57974a2a..3b4d42c9 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -32,6 +32,7 @@ import 'package:weblibre/core/providers/device_info.dart'; import 'package:weblibre/core/providers/router.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; +import 'package:weblibre/features/bangs/domain/services/search_history_cleanup.dart'; import 'package:weblibre/features/geckoview/domain/providers.dart'; import 'package:weblibre/features/geckoview/domain/providers/browser_extension.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; @@ -451,6 +452,19 @@ class _BrowserViewState extends ConsumerState }, ); + ref.listenManual( + fireImmediately: true, + searchHistoryCleanupServiceProvider, + (previous, next) {}, + onError: (error, stackTrace) { + logger.e( + 'Error listening to searchHistoryCleanupServiceProvider', + error: error, + stackTrace: stackTrace, + ); + }, + ); + ref.listenManual( fireImmediately: true, preferenceFixatorProvider, diff --git a/app/lib/features/settings/presentation/screens/search_content_settings.dart b/app/lib/features/settings/presentation/screens/search_content_settings.dart index 1f4e8ef2..dde21c50 100644 --- a/app/lib/features/settings/presentation/screens/search_content_settings.dart +++ b/app/lib/features/settings/presentation/screens/search_content_settings.dart @@ -19,6 +19,7 @@ */ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; @@ -72,6 +73,7 @@ class _SearchSection extends StatelessWidget { _DefaultSearchProviderSection(), _BangsTile(), _AutocompleteProviderSection(), + _MaxSearchHistoryEntriesSection(), ], ); } @@ -234,6 +236,72 @@ class _BangsTile extends StatelessWidget { } } +class _MaxSearchHistoryEntriesSection extends HookConsumerWidget { + const _MaxSearchHistoryEntriesSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + + final maxSearchHistoryEntries = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.maxSearchHistoryEntries, + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Search History Limit'), + subtitle: Text('Maximum number of recent searches to remember'), + leading: Icon(MdiIcons.history), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40.0), + child: Form( + key: formKey, + child: TextFormField( + initialValue: maxSearchHistoryEntries.toString(), + keyboardType: TextInputType.number, + decoration: const InputDecoration(suffixText: 'entries'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + final parsedValue = int.tryParse(value); + if (parsedValue == null) { + return 'Please enter a valid number'; + } + if (parsedValue < 0 || parsedValue > 100) { + return 'Value must be between 0 and 100'; + } + return null; + }, + onFieldSubmitted: (value) async { + if (formKey.currentState?.validate() ?? false) { + final parsedValue = int.parse(value); + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .maxSearchHistoryEntries(parsedValue), + ); + } + }, + ), + ), + ), + ], + ), + ); + } +} + class _OnDeviceAiTile extends HookConsumerWidget { const _OnDeviceAiTile(); diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 4dfa4a40..cca8fc25 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -83,6 +83,7 @@ class GeneralSettings with FastEquatable { final bool pullToRefreshEnabled; final bool doubleBackCloseTab; final Duration unassignedTabsAutoCleanInterval; + final int maxSearchHistoryEntries; GeneralSettings({ required this.themeMode, @@ -108,6 +109,7 @@ class GeneralSettings with FastEquatable { required this.pullToRefreshEnabled, required this.doubleBackCloseTab, required this.unassignedTabsAutoCleanInterval, + required this.maxSearchHistoryEntries, }); GeneralSettings.withDefaults({ @@ -134,6 +136,7 @@ class GeneralSettings with FastEquatable { bool? pullToRefreshEnabled, bool? doubleBackCloseTab, Duration? unassignedTabsAutoCleanInterval, + int? maxSearchHistoryEntries, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -160,7 +163,8 @@ class GeneralSettings with FastEquatable { pullToRefreshEnabled = pullToRefreshEnabled ?? true, doubleBackCloseTab = doubleBackCloseTab ?? true, unassignedTabsAutoCleanInterval = - unassignedTabsAutoCleanInterval ?? Duration.zero; + unassignedTabsAutoCleanInterval ?? Duration.zero, + maxSearchHistoryEntries = maxSearchHistoryEntries ?? 5; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -192,5 +196,6 @@ class GeneralSettings with FastEquatable { pullToRefreshEnabled, doubleBackCloseTab, unassignedTabsAutoCleanInterval, + maxSearchHistoryEntries, ]; } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index 26bf0527..2b73af13 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -65,6 +65,8 @@ abstract class _$GeneralSettingsCWProxy { Duration unassignedTabsAutoCleanInterval, ); + GeneralSettings maxSearchHistoryEntries(int maxSearchHistoryEntries); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// @@ -96,6 +98,7 @@ abstract class _$GeneralSettingsCWProxy { bool pullToRefreshEnabled, bool doubleBackCloseTab, Duration unassignedTabsAutoCleanInterval, + int maxSearchHistoryEntries, }); } @@ -203,6 +206,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Duration unassignedTabsAutoCleanInterval, ) => call(unassignedTabsAutoCleanInterval: unassignedTabsAutoCleanInterval); + @override + GeneralSettings maxSearchHistoryEntries(int maxSearchHistoryEntries) => + call(maxSearchHistoryEntries: maxSearchHistoryEntries); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. @@ -235,6 +242,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? pullToRefreshEnabled = const $CopyWithPlaceholder(), Object? doubleBackCloseTab = const $CopyWithPlaceholder(), Object? unassignedTabsAutoCleanInterval = const $CopyWithPlaceholder(), + Object? maxSearchHistoryEntries = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null @@ -371,6 +379,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.unassignedTabsAutoCleanInterval // ignore: cast_nullable_to_non_nullable : unassignedTabsAutoCleanInterval as Duration, + maxSearchHistoryEntries: + maxSearchHistoryEntries == const $CopyWithPlaceholder() || + maxSearchHistoryEntries == null + ? _value.maxSearchHistoryEntries + // ignore: cast_nullable_to_non_nullable + : maxSearchHistoryEntries as int, ); } } @@ -444,6 +458,7 @@ GeneralSettings _$GeneralSettingsFromJson( microseconds: (json['unassignedTabsAutoCleanInterval'] as num) .toInt(), ), + maxSearchHistoryEntries: (json['maxSearchHistoryEntries'] as num?)?.toInt(), ); Map _$GeneralSettingsToJson( @@ -481,6 +496,7 @@ Map _$GeneralSettingsToJson( 'doubleBackCloseTab': instance.doubleBackCloseTab, 'unassignedTabsAutoCleanInterval': instance.unassignedTabsAutoCleanInterval.inMicroseconds, + 'maxSearchHistoryEntries': instance.maxSearchHistoryEntries, }; const _$ThemeModeEnumMap = { diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index 4cbb806d..e9ad04d0 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -135,6 +135,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.int, db.typeMapping, ), + 'maxSearchHistoryEntries': settings['maxSearchHistoryEntries']?.readAs( + DriftSqlType.int, + db.typeMapping, + ), }); } diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index e4a8cd71..aaeab871 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'08af8820ddd41036ae0d9531c87cf17c9c62db0a'; + r'0d4b01ff15942b513a7cdceee7bf43721ae1c074'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {