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/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<BangData?> 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()
@@ -7,7 +7,7 @@ part of 'bangs.dart';
// **************************************************************************
String _$defaultSearchBangDataHash() =>
r'c93026fe7b1650fe8a39bb2a8850af3ee416c306';
r'acce519559903505f9623f3631b6f4177a396d39';
/// See also [defaultSearchBangData].
@ProviderFor(defaultSearchBangData)
@@ -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<String?>(
context,
);
if (trigger != null) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(trigger);
}
},
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: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);
},
);
},
@@ -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(),
@@ -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<String?>(context);
if (trigger != null) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(trigger);
}
},
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: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(
@@ -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 {
final ThemeMode themeMode;
final bool enableReadability;
final bool enforceReadability;
final Set<DeleteBrowsingDataType>? 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<String, dynamic> json) =>
_$GeneralSettingsFromJson(json);
@@ -48,6 +56,8 @@ class GeneralSettings with FastEquatable {
List<Object?> get hashParameters => [
themeMode,
enableReadability,
enforceReadability,
deleteBrowsingDataOnQuit,
defaultSearchProvider,
];
}
@@ -11,10 +11,14 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings enableReadability(bool enableReadability);
GeneralSettings enforceReadability(bool enforceReadability);
GeneralSettings 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.
///
/// Usage
@@ -24,7 +28,9 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings call({
ThemeMode themeMode,
bool enableReadability,
bool enforceReadability,
Set<DeleteBrowsingDataType>? 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<DeleteBrowsingDataType>? 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<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(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
enableReadability: json['enableReadability'] as bool?,
enforceReadability: json['enforceReadability'] as bool?,
deleteBrowsingDataOnQuit:
(json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e))
.toSet(),
defaultSearchProvider: json['defaultSearchProvider'] as String?,
);
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
<String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'enableReadability': instance.enableReadability,
'enforceReadability': instance.enforceReadability,
'deleteBrowsingDataOnQuit':
instance.deleteBrowsingDataOnQuit
?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!)
.toList(),
'defaultSearchProvider': instance.defaultSearchProvider,
};
const _$ThemeModeEnumMap = {
@@ -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,
),
});
}
@@ -7,7 +7,7 @@ part of 'general_settings.dart';
// **************************************************************************
String _$generalSettingsRepositoryHash() =>
r'001ffa37f5afe1d2d3ee2b3ce8539836af76cf89';
r'2dcc600ccfb074fa27fae90acb3d61e10c87e884';
/// See also [GeneralSettingsRepository].
@ProviderFor(GeneralSettingsRepository)