settings for readability & default search

This commit is contained in:
Fabian Freund
2025-05-17 19:47:56 +02:00
parent baa67d2c0b
commit 9af6ff6630
12 changed files with 212 additions and 33 deletions
@@ -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/data/models/search_history_entry.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/bangs/domain/repositories/sync.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/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -10,9 +11,14 @@ part 'bangs.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
Stream<BangData?> defaultSearchBangData(Ref ref) { Stream<BangData?> defaultSearchBangData(Ref ref) {
final trigger = ref.watch(
generalSettingsRepositoryProvider.select(
(value) => value.defaultSearchProvider,
),
);
final repository = ref.watch(bangDataRepositoryProvider.notifier); final repository = ref.watch(bangDataRepositoryProvider.notifier);
//TODO: Setting return repository.watchBang(trigger);
return repository.watchBang('lai');
} }
@Riverpod() @Riverpod()
@@ -7,7 +7,7 @@ part of 'bangs.dart';
// ************************************************************************** // **************************************************************************
String _$defaultSearchBangDataHash() => String _$defaultSearchBangDataHash() =>
r'c93026fe7b1650fe8a39bb2a8850af3ee416c306'; r'acce519559903505f9623f3631b6f4177a396d39';
/// See also [defaultSearchBangData]. /// See also [defaultSearchBangData].
@ProviderFor(defaultSearchBangData) @ProviderFor(defaultSearchBangData)
@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.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'; import 'package:lensai/presentation/widgets/failure_widget.dart';
class BangCategoriesScreen extends HookConsumerWidget { class BangCategoriesScreen extends HookConsumerWidget {
@@ -18,7 +19,15 @@ class BangCategoriesScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
await const BangSearchRoute().push(context); final trigger = await const BangSearchRoute().push<String?>(
context,
);
if (trigger != null) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(trigger);
}
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
), ),
@@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_details.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/features/user/domain/providers.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
@@ -70,11 +69,7 @@ class BangSearchScreen extends HookConsumerWidget {
return BangDetails( return BangDetails(
bang, bang,
onTap: () { onTap: () {
ref context.pop(bang.trigger);
.read(selectedBangTriggerProvider().notifier)
.setTrigger(bang.trigger);
context.pop();
}, },
); );
}, },
@@ -20,6 +20,12 @@ class ReaderButton extends HookConsumerWidget {
), ),
); );
final enforceReadability = ref.watch(
generalSettingsRepositoryProvider.select(
(value) => value.enforceReadability,
),
);
final readerabilityState = ref.watch( final readerabilityState = ref.watch(
selectedTabStateProvider.select( selectedTabStateProvider.select(
(state) => state?.readerableState ?? ReaderableState.$default(), (state) => state?.readerableState ?? ReaderableState.$default(),
@@ -39,29 +45,27 @@ class ReaderButton extends HookConsumerWidget {
return Visibility( return Visibility(
visible: visible:
readerabilityState.readerable && (readerabilityState.readerable &&
(enableReadability || readerabilityState.active), (enableReadability || readerabilityState.active)) ||
(enforceReadability && enableReadability),
child: readerChanging.when( child: readerChanging.when(
skipLoadingOnReload: true, skipLoadingOnReload: true,
data: data:
(_) => Visibility( (_) => InkWell(
visible: readerabilityState.readerable, onTap:
child: InkWell( readerChanging.isLoading
onTap: ? null
readerChanging.isLoading : () async {
? null await ref
: () async { .read(readerableScreenControllerProvider.notifier)
await ref .toggleReaderView(!readerabilityState.active);
.read(readerableScreenControllerProvider.notifier) },
.toggleReaderView(!readerabilityState.active); child: Padding(
}, padding: const EdgeInsets.symmetric(
child: Padding( vertical: 15.0,
padding: const EdgeInsets.symmetric( horizontal: 8.0,
vertical: 15.0,
horizontal: 8.0,
),
child: icon,
), ),
child: icon,
), ),
), ),
error: (error, stackTrace) => SizedBox.shrink(), error: (error, stackTrace) => SizedBox.shrink(),
@@ -4,6 +4,7 @@ import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/search.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/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart';
import 'package:lensai/presentation/widgets/url_icon.dart'; import 'package:lensai/presentation/widgets/url_icon.dart';
@@ -98,12 +99,18 @@ class BangChips extends HookConsumerWidget {
onPressed: () async { onPressed: () async {
final searchText = searchTextController?.text.trim(); final searchText = searchTextController?.text.trim();
await BangSearchRoute( final trigger = await BangSearchRoute(
searchText: searchText:
(searchText.isEmpty) (searchText.isEmpty)
? BangSearchRoute.emptySearchText ? BangSearchRoute.emptySearchText
: searchText!, : searchText!,
).push(context); ).push<String?>(context);
if (trigger != null) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(trigger);
}
}, },
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
), ),
@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/settings/presentation/controllers/save_settings.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/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/data/models/general_settings.dart';
import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/features/user/domain/repositories/cache.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( SwitchListTile.adaptive(
title: const Text('Enable Reader Mode'), title: const Text('Enable Reader Mode'),
subtitle: const Text( 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( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final size = ref.watch( final size = ref.watch(
@@ -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<void> 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<String?>(
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),
);
}
}
@@ -24,20 +24,28 @@ enum DeleteBrowsingDataType {
class GeneralSettings with FastEquatable { class GeneralSettings with FastEquatable {
final ThemeMode themeMode; final ThemeMode themeMode;
final bool enableReadability; final bool enableReadability;
final bool enforceReadability;
final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit; final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit;
final String defaultSearchProvider;
GeneralSettings({ GeneralSettings({
required this.themeMode, required this.themeMode,
required this.enableReadability, required this.enableReadability,
required this.enforceReadability,
required this.deleteBrowsingDataOnQuit, required this.deleteBrowsingDataOnQuit,
required this.defaultSearchProvider,
}); });
GeneralSettings.withDefaults({ GeneralSettings.withDefaults({
ThemeMode? themeMode, ThemeMode? themeMode,
bool? enableReadability, bool? enableReadability,
bool? enforceReadability,
this.deleteBrowsingDataOnQuit, this.deleteBrowsingDataOnQuit,
String? defaultSearchProvider,
}) : themeMode = themeMode ?? ThemeMode.dark, }) : themeMode = themeMode ?? ThemeMode.dark,
enableReadability = enableReadability ?? true; enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false,
defaultSearchProvider = defaultSearchProvider ?? 'lais';
factory GeneralSettings.fromJson(Map<String, dynamic> json) => factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
_$GeneralSettingsFromJson(json); _$GeneralSettingsFromJson(json);
@@ -48,6 +56,8 @@ class GeneralSettings with FastEquatable {
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
themeMode, themeMode,
enableReadability, enableReadability,
enforceReadability,
deleteBrowsingDataOnQuit, deleteBrowsingDataOnQuit,
defaultSearchProvider,
]; ];
} }
@@ -11,10 +11,14 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings enableReadability(bool enableReadability); GeneralSettings enableReadability(bool enableReadability);
GeneralSettings enforceReadability(bool enforceReadability);
GeneralSettings deleteBrowsingDataOnQuit( GeneralSettings deleteBrowsingDataOnQuit(
Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit, Set<DeleteBrowsingDataType>? 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. /// 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 /// Usage
@@ -24,7 +28,9 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings call({ GeneralSettings call({
ThemeMode themeMode, ThemeMode themeMode,
bool enableReadability, bool enableReadability,
bool enforceReadability,
Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit, Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit,
String defaultSearchProvider,
}); });
} }
@@ -41,11 +47,19 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings enableReadability(bool enableReadability) => GeneralSettings enableReadability(bool enableReadability) =>
this(enableReadability: enableReadability); this(enableReadability: enableReadability);
@override
GeneralSettings enforceReadability(bool enforceReadability) =>
this(enforceReadability: enforceReadability);
@override @override
GeneralSettings deleteBrowsingDataOnQuit( GeneralSettings deleteBrowsingDataOnQuit(
Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit, Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit,
) => this(deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit); ) => this(deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit);
@override
GeneralSettings defaultSearchProvider(String defaultSearchProvider) =>
this(defaultSearchProvider: defaultSearchProvider);
@override @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. /// 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({ GeneralSettings call({
Object? themeMode = const $CopyWithPlaceholder(), Object? themeMode = const $CopyWithPlaceholder(),
Object? enableReadability = const $CopyWithPlaceholder(), Object? enableReadability = const $CopyWithPlaceholder(),
Object? enforceReadability = const $CopyWithPlaceholder(),
Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(), Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(),
Object? defaultSearchProvider = const $CopyWithPlaceholder(),
}) { }) {
return GeneralSettings( return GeneralSettings(
themeMode: themeMode:
@@ -69,11 +85,21 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.enableReadability ? _value.enableReadability
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: enableReadability as bool, : enableReadability as bool,
enforceReadability:
enforceReadability == const $CopyWithPlaceholder()
? _value.enforceReadability
// ignore: cast_nullable_to_non_nullable
: enforceReadability as bool,
deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit:
deleteBrowsingDataOnQuit == const $CopyWithPlaceholder() deleteBrowsingDataOnQuit == const $CopyWithPlaceholder()
? _value.deleteBrowsingDataOnQuit ? _value.deleteBrowsingDataOnQuit
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: deleteBrowsingDataOnQuit as Set<DeleteBrowsingDataType>?, : deleteBrowsingDataOnQuit as Set<DeleteBrowsingDataType>?,
defaultSearchProvider:
defaultSearchProvider == const $CopyWithPlaceholder()
? _value.defaultSearchProvider
// ignore: cast_nullable_to_non_nullable
: defaultSearchProvider as String,
); );
} }
} }
@@ -92,20 +118,24 @@ GeneralSettings _$GeneralSettingsFromJson(Map<String, dynamic> json) =>
GeneralSettings.withDefaults( GeneralSettings.withDefaults(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
enableReadability: json['enableReadability'] as bool?, enableReadability: json['enableReadability'] as bool?,
enforceReadability: json['enforceReadability'] as bool?,
deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit:
(json['deleteBrowsingDataOnQuit'] as List<dynamic>?) (json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e)) ?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e))
.toSet(), .toSet(),
defaultSearchProvider: json['defaultSearchProvider'] as String?,
); );
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) => Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
<String, dynamic>{ <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'enableReadability': instance.enableReadability, 'enableReadability': instance.enableReadability,
'enforceReadability': instance.enforceReadability,
'deleteBrowsingDataOnQuit': 'deleteBrowsingDataOnQuit':
instance.deleteBrowsingDataOnQuit instance.deleteBrowsingDataOnQuit
?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!)
.toList(), .toList(),
'defaultSearchProvider': instance.defaultSearchProvider,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@@ -32,9 +32,17 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool, DriftSqlType.bool,
db.typeMapping, db.typeMapping,
), ),
'enforceReadability': settings['enforceReadability']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
'deleteBrowsingDataOnQuit': settings['deleteBrowsingDataOnQuit'] 'deleteBrowsingDataOnQuit': settings['deleteBrowsingDataOnQuit']
?.readAs(DriftSqlType.string, db.typeMapping) ?.readAs(DriftSqlType.string, db.typeMapping)
.mapNotNull(jsonDecode), .mapNotNull(jsonDecode),
'defaultSearchProvider': settings['defaultSearchProvider']?.readAs(
DriftSqlType.string,
db.typeMapping,
),
}); });
} }
@@ -7,7 +7,7 @@ part of 'general_settings.dart';
// ************************************************************************** // **************************************************************************
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'001ffa37f5afe1d2d3ee2b3ce8539836af76cf89'; r'2dcc600ccfb074fa27fae90acb3d61e10c87e884';
/// See also [GeneralSettingsRepository]. /// See also [GeneralSettingsRepository].
@ProviderFor(GeneralSettingsRepository) @ProviderFor(GeneralSettingsRepository)