From 9af6ff663056cc2515f35911e6ea2be78283279d Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 17 May 2025 19:47:56 +0200 Subject: [PATCH] settings for readability & default search --- .../bangs/domain/providers/bangs.dart | 10 ++- .../bangs/domain/providers/bangs.g.dart | 2 +- .../presentation/screens/categories.dart | 11 ++- .../bangs/presentation/screens/search.dart | 7 +- .../presentation/widgets/reader_button.dart | 42 ++++++------ .../presentation/widgets/bang_chips.dart | 11 ++- .../screens/general_settings.dart | 42 ++++++++++++ .../widgets/default_search_selector.dart | 68 +++++++++++++++++++ .../user/data/models/general_settings.dart | 12 +++- .../user/data/models/general_settings.g.dart | 30 ++++++++ .../domain/repositories/general_settings.dart | 8 +++ .../repositories/general_settings.g.dart | 2 +- 12 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 app/lib/features/settings/presentation/widgets/default_search_selector.dart diff --git a/app/lib/features/bangs/domain/providers/bangs.dart b/app/lib/features/bangs/domain/providers/bangs.dart index 230b4aa2..c6207551 100644 --- a/app/lib/features/bangs/domain/providers/bangs.dart +++ b/app/lib/features/bangs/domain/providers/bangs.dart @@ -3,6 +3,7 @@ import 'package:lensai/features/bangs/data/models/bang_group.dart'; import 'package:lensai/features/bangs/data/models/search_history_entry.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/domain/repositories/sync.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,9 +11,14 @@ part 'bangs.g.dart'; @Riverpod(keepAlive: true) Stream defaultSearchBangData(Ref ref) { + final trigger = ref.watch( + generalSettingsRepositoryProvider.select( + (value) => value.defaultSearchProvider, + ), + ); + final repository = ref.watch(bangDataRepositoryProvider.notifier); - //TODO: Setting - return repository.watchBang('lai'); + return repository.watchBang(trigger); } @Riverpod() diff --git a/app/lib/features/bangs/domain/providers/bangs.g.dart b/app/lib/features/bangs/domain/providers/bangs.g.dart index 6c09bcf0..bf829157 100644 --- a/app/lib/features/bangs/domain/providers/bangs.g.dart +++ b/app/lib/features/bangs/domain/providers/bangs.g.dart @@ -7,7 +7,7 @@ part of 'bangs.dart'; // ************************************************************************** String _$defaultSearchBangDataHash() => - r'c93026fe7b1650fe8a39bb2a8850af3ee416c306'; + r'acce519559903505f9623f3631b6f4177a396d39'; /// See also [defaultSearchBangData]. @ProviderFor(defaultSearchBangData) diff --git a/app/lib/features/bangs/presentation/screens/categories.dart b/app/lib/features/bangs/presentation/screens/categories.dart index d191b6d3..4b5a16e7 100644 --- a/app/lib/features/bangs/presentation/screens/categories.dart +++ b/app/lib/features/bangs/presentation/screens/categories.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; class BangCategoriesScreen extends HookConsumerWidget { @@ -18,7 +19,15 @@ class BangCategoriesScreen extends HookConsumerWidget { actions: [ IconButton( onPressed: () async { - await const BangSearchRoute().push(context); + final trigger = await const BangSearchRoute().push( + context, + ); + + if (trigger != null) { + ref + .read(selectedBangTriggerProvider().notifier) + .setTrigger(trigger); + } }, icon: const Icon(Icons.search), ), diff --git a/app/lib/features/bangs/presentation/screens/search.dart b/app/lib/features/bangs/presentation/screens/search.dart index f495bf3e..1e596a57 100644 --- a/app/lib/features/bangs/presentation/screens/search.dart +++ b/app/lib/features/bangs/presentation/screens/search.dart @@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; -import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; @@ -70,11 +69,7 @@ class BangSearchScreen extends HookConsumerWidget { return BangDetails( bang, onTap: () { - ref - .read(selectedBangTriggerProvider().notifier) - .setTrigger(bang.trigger); - - context.pop(); + context.pop(bang.trigger); }, ); }, diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart index 2fc5b543..5aeb7546 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart @@ -20,6 +20,12 @@ class ReaderButton extends HookConsumerWidget { ), ); + final enforceReadability = ref.watch( + generalSettingsRepositoryProvider.select( + (value) => value.enforceReadability, + ), + ); + final readerabilityState = ref.watch( selectedTabStateProvider.select( (state) => state?.readerableState ?? ReaderableState.$default(), @@ -39,29 +45,27 @@ class ReaderButton extends HookConsumerWidget { return Visibility( visible: - readerabilityState.readerable && - (enableReadability || readerabilityState.active), + (readerabilityState.readerable && + (enableReadability || readerabilityState.active)) || + (enforceReadability && enableReadability), child: readerChanging.when( skipLoadingOnReload: true, data: - (_) => Visibility( - visible: readerabilityState.readerable, - child: InkWell( - onTap: - readerChanging.isLoading - ? null - : () async { - await ref - .read(readerableScreenControllerProvider.notifier) - .toggleReaderView(!readerabilityState.active); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: icon, + (_) => InkWell( + onTap: + readerChanging.isLoading + ? null + : () async { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(!readerabilityState.active); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, ), + child: icon, ), ), error: (error, stackTrace) => SizedBox.shrink(), diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart b/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart index 43e6d24f..39a8a4f5 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart @@ -4,6 +4,7 @@ import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; import 'package:lensai/presentation/widgets/url_icon.dart'; @@ -98,12 +99,18 @@ class BangChips extends HookConsumerWidget { onPressed: () async { final searchText = searchTextController?.text.trim(); - await BangSearchRoute( + final trigger = await BangSearchRoute( searchText: (searchText.isEmpty) ? BangSearchRoute.emptySearchText : searchText!, - ).push(context); + ).push(context); + + if (trigger != null) { + ref + .read(selectedBangTriggerProvider().notifier) + .setTrigger(trigger); + } }, icon: const Icon(Icons.chevron_right), ), diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index 5a25cb96..632015aa 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart'; +import 'package:lensai/features/settings/presentation/widgets/default_search_selector.dart'; import 'package:lensai/features/user/data/models/general_settings.dart'; import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart'; @@ -74,6 +75,24 @@ class GeneralSettingsScreen extends HookConsumerWidget { ], ), ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text('Default Search Provider'), + leading: Icon(MdiIcons.cloudSearch), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: EdgeInsets.only(left: 40), + child: DefaultSearchSelector(), + ), + ], + ), + ), SwitchListTile.adaptive( title: const Text('Enable Reader Mode'), subtitle: const Text( @@ -90,6 +109,29 @@ class GeneralSettingsScreen extends HookConsumerWidget { ); }, ), + SwitchListTile.adaptive( + title: const Text('Enforce Reader Mode'), + subtitle: const Text( + 'Override readability probability of websites and always show Reader Mode capabilities even the site might not be compatible.', + ), + secondary: const Icon(MdiIcons.bookCheck), + value: + generalSettings.enableReadability && + generalSettings.enforceReadability, + onChanged: + generalSettings.enableReadability + ? (value) async { + await ref + .read( + saveGeneralSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .enforceReadability(value), + ); + } + : null, + ), Consumer( builder: (context, ref, child) { final size = ref.watch( diff --git a/app/lib/features/settings/presentation/widgets/default_search_selector.dart b/app/lib/features/settings/presentation/widgets/default_search_selector.dart new file mode 100644 index 00000000..7516760c --- /dev/null +++ b/app/lib/features/settings/presentation/widgets/default_search_selector.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/features/bangs/domain/providers/bangs.dart'; +import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; +import 'package:lensai/features/user/data/models/general_settings.dart'; +import 'package:lensai/presentation/widgets/selectable_chips.dart'; +import 'package:lensai/presentation/widgets/url_icon.dart'; + +class DefaultSearchSelector extends HookConsumerWidget { + const DefaultSearchSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final activeBang = ref.watch(defaultSearchBangDataProvider).valueOrNull; + final availableBangs = ref.watch(frequentBangListProvider); + + Future updateSearchProvider(String trigger) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.defaultSearchProvider(trigger), + ); + } + + return availableBangs.when( + skipLoadingOnReload: true, + data: (availableBangs) { + return SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: SelectableChips( + itemId: (bang) => bang.trigger, + itemAvatar: + (bang) => + UrlIcon([bang.getTemplateUrl('')], iconSize: 20), + itemLabel: (bang) => Text(bang.websiteName), + availableItems: availableBangs, + selectedItem: activeBang, + onSelected: (bang) async { + await updateSearchProvider(bang.trigger); + }, + ), + ), + IconButton( + onPressed: () async { + final trigger = await const BangSearchRoute().push( + context, + ); + + if (trigger != null) { + await updateSearchProvider(trigger); + } + }, + icon: const Icon(Icons.chevron_right), + ), + ], + ), + ); + }, + error: (error, stackTrace) => const SizedBox.shrink(), + loading: () => const SizedBox(height: 48, width: double.infinity), + ); + } +} diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 89b2a487..2ff8a226 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -24,20 +24,28 @@ enum DeleteBrowsingDataType { class GeneralSettings with FastEquatable { final ThemeMode themeMode; final bool enableReadability; + final bool enforceReadability; final Set? deleteBrowsingDataOnQuit; + final String defaultSearchProvider; GeneralSettings({ required this.themeMode, required this.enableReadability, + required this.enforceReadability, required this.deleteBrowsingDataOnQuit, + required this.defaultSearchProvider, }); GeneralSettings.withDefaults({ ThemeMode? themeMode, bool? enableReadability, + bool? enforceReadability, this.deleteBrowsingDataOnQuit, + String? defaultSearchProvider, }) : themeMode = themeMode ?? ThemeMode.dark, - enableReadability = enableReadability ?? true; + enableReadability = enableReadability ?? true, + enforceReadability = enforceReadability ?? false, + defaultSearchProvider = defaultSearchProvider ?? 'lais'; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -48,6 +56,8 @@ class GeneralSettings with FastEquatable { List get hashParameters => [ themeMode, enableReadability, + enforceReadability, deleteBrowsingDataOnQuit, + defaultSearchProvider, ]; } 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 74966fa9..c67a6f82 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -11,10 +11,14 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings enableReadability(bool enableReadability); + GeneralSettings enforceReadability(bool enforceReadability); + GeneralSettings deleteBrowsingDataOnQuit( Set? deleteBrowsingDataOnQuit, ); + GeneralSettings defaultSearchProvider(String defaultSearchProvider); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -24,7 +28,9 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings call({ ThemeMode themeMode, bool enableReadability, + bool enforceReadability, Set? deleteBrowsingDataOnQuit, + String defaultSearchProvider, }); } @@ -41,11 +47,19 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings enableReadability(bool enableReadability) => this(enableReadability: enableReadability); + @override + GeneralSettings enforceReadability(bool enforceReadability) => + this(enforceReadability: enforceReadability); + @override GeneralSettings deleteBrowsingDataOnQuit( Set? deleteBrowsingDataOnQuit, ) => this(deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit); + @override + GeneralSettings defaultSearchProvider(String defaultSearchProvider) => + this(defaultSearchProvider: defaultSearchProvider); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -56,7 +70,9 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings call({ Object? themeMode = const $CopyWithPlaceholder(), Object? enableReadability = const $CopyWithPlaceholder(), + Object? enforceReadability = const $CopyWithPlaceholder(), Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(), + Object? defaultSearchProvider = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: @@ -69,11 +85,21 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.enableReadability // ignore: cast_nullable_to_non_nullable : enableReadability as bool, + enforceReadability: + enforceReadability == const $CopyWithPlaceholder() + ? _value.enforceReadability + // ignore: cast_nullable_to_non_nullable + : enforceReadability as bool, deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit == const $CopyWithPlaceholder() ? _value.deleteBrowsingDataOnQuit // ignore: cast_nullable_to_non_nullable : deleteBrowsingDataOnQuit as Set?, + defaultSearchProvider: + defaultSearchProvider == const $CopyWithPlaceholder() + ? _value.defaultSearchProvider + // ignore: cast_nullable_to_non_nullable + : defaultSearchProvider as String, ); } } @@ -92,20 +118,24 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => GeneralSettings.withDefaults( themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), enableReadability: json['enableReadability'] as bool?, + enforceReadability: json['enforceReadability'] as bool?, deleteBrowsingDataOnQuit: (json['deleteBrowsingDataOnQuit'] as List?) ?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e)) .toSet(), + defaultSearchProvider: json['defaultSearchProvider'] as String?, ); Map _$GeneralSettingsToJson(GeneralSettings instance) => { 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'enableReadability': instance.enableReadability, + 'enforceReadability': instance.enforceReadability, 'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) .toList(), + 'defaultSearchProvider': instance.defaultSearchProvider, }; 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 6b3a6d4e..9319911c 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -32,9 +32,17 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'enforceReadability': settings['enforceReadability']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), 'deleteBrowsingDataOnQuit': settings['deleteBrowsingDataOnQuit'] ?.readAs(DriftSqlType.string, db.typeMapping) .mapNotNull(jsonDecode), + 'defaultSearchProvider': settings['defaultSearchProvider']?.readAs( + DriftSqlType.string, + 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 92650e4c..5408ea51 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -7,7 +7,7 @@ part of 'general_settings.dart'; // ************************************************************************** String _$generalSettingsRepositoryHash() => - r'001ffa37f5afe1d2d3ee2b3ce8539836af76cf89'; + r'2dcc600ccfb074fa27fae90acb3d61e10c87e884'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)