refactorings and cleanup

This commit is contained in:
Fabian Freund
2026-03-10 04:34:58 +01:00
parent 3a1bacb746
commit 36bdb5b966
8 changed files with 204 additions and 157 deletions
@@ -33,7 +33,7 @@ final class BangSearchProvider
BangSearch create() => BangSearch(); BangSearch create() => BangSearch();
} }
String _$bangSearchHash() => r'7993bba3765d24ca9a7a17eca86ffdbc7c1b5e65'; String _$bangSearchHash() => r'feed24edfe703b0697f4a855be9c7359c456b0f2';
abstract class _$BangSearch extends $StreamNotifier<List<BangData>> { abstract class _$BangSearch extends $StreamNotifier<List<BangData>> {
Stream<List<BangData>> build(); Stream<List<BangData>> build();
@@ -19,6 +19,8 @@
*/ */
import 'dart:convert'; import 'dart:convert';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod/experimental/persist.dart';
import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/experimental/persist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -27,14 +29,30 @@ import 'package:weblibre/features/user/data/providers.dart';
part 'search_module_order.g.dart'; part 'search_module_order.g.dart';
typedef ModuleOrderEntry = ({SearchModuleType type, bool visible}); @JsonSerializable()
class ModuleOrderEntry with FastEquatable {
final SearchModuleType type;
final bool visible;
ModuleOrderEntry({required this.type, required this.visible});
factory ModuleOrderEntry.fromJson(Map<String, dynamic> json) =>
_$ModuleOrderEntryFromJson(json);
Map<String, dynamic> toJson() => _$ModuleOrderEntryToJson(this);
@override
List<Object?> get hashParameters => [type, visible];
}
List<ModuleOrderEntry> _mergeWithDefaults( List<ModuleOrderEntry> _mergeWithDefaults(
List<ModuleOrderEntry>? persisted, List<ModuleOrderEntry>? persisted,
List<SearchModuleType> defaults, List<SearchModuleType> defaults,
) { ) {
if (persisted == null) { if (persisted == null) {
return defaults.map((type) => (type: type, visible: true)).toList(); return defaults
.map((type) => ModuleOrderEntry(type: type, visible: true))
.toList();
} }
final defaultSet = defaults.toSet(); final defaultSet = defaults.toSet();
@@ -44,7 +62,7 @@ List<ModuleOrderEntry> _mergeWithDefaults(
final persistedTypes = result.map((e) => e.type).toSet(); final persistedTypes = result.map((e) => e.type).toSet();
for (final type in defaults) { for (final type in defaults) {
if (!persistedTypes.contains(type)) { if (!persistedTypes.contains(type)) {
result.add((type: type, visible: true)); result.add(ModuleOrderEntry(type: type, visible: true));
} }
} }
return result; return result;
@@ -63,7 +81,10 @@ class SearchModuleOrder extends _$SearchModuleOrder {
void toggleVisibility(SearchModuleType type) { void toggleVisibility(SearchModuleType type) {
state = [ state = [
for (final e in state) for (final e in state)
if (e.type == type) (type: e.type, visible: !e.visible) else e, if (e.type == type)
ModuleOrderEntry(type: e.type, visible: !e.visible)
else
e,
]; ];
} }
@@ -72,20 +93,13 @@ class SearchModuleOrder extends _$SearchModuleOrder {
persist( persist(
ref.watch(riverpodDatabaseStorageProvider), ref.watch(riverpodDatabaseStorageProvider),
key: group.key, key: group.key,
encode: (state) => jsonEncode( encode: (state) => jsonEncode(state.map((e) => e.toJson()).toList()),
state
.map((e) => {'type': e.type.name, 'visible': e.visible})
.toList(),
),
decode: (encoded) { decode: (encoded) {
final decoded = (jsonDecode(encoded) as List<dynamic>) final decoded = (jsonDecode(encoded) as List<dynamic>)
.cast<Map<String, dynamic>>() .cast<Map<String, dynamic>>()
.map((e) { .map((e) {
try { try {
return ( return ModuleOrderEntry.fromJson(e);
type: SearchModuleType.values.byName(e['type']! as String),
visible: e['visible']! as bool,
);
} catch (_) { } catch (_) {
return null; return null;
} }
@@ -99,7 +113,7 @@ class SearchModuleOrder extends _$SearchModuleOrder {
return stateOrNull ?? return stateOrNull ??
group.defaultModules group.defaultModules
.map((type) => (type: type, visible: true)) .map((type) => ModuleOrderEntry(type: type, visible: true))
.toList(); .toList();
} }
} }
@@ -2,6 +2,35 @@
part of 'search_module_order.dart'; part of 'search_module_order.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ModuleOrderEntry _$ModuleOrderEntryFromJson(Map<String, dynamic> json) =>
ModuleOrderEntry(
type: $enumDecode(_$SearchModuleTypeEnumMap, json['type']),
visible: json['visible'] as bool,
);
Map<String, dynamic> _$ModuleOrderEntryToJson(ModuleOrderEntry instance) =>
<String, dynamic>{
'type': _$SearchModuleTypeEnumMap[instance.type]!,
'visible': instance.visible,
};
const _$SearchModuleTypeEnumMap = {
SearchModuleType.tabs: 'tabs',
SearchModuleType.articles: 'articles',
SearchModuleType.bookmarks: 'bookmarks',
SearchModuleType.history: 'history',
SearchModuleType.historyHighlights: 'historyHighlights',
SearchModuleType.topSites: 'topSites',
SearchModuleType.recentHistory: 'recentHistory',
SearchModuleType.recentArticles: 'recentArticles',
SearchModuleType.recentTabs: 'recentTabs',
SearchModuleType.containers: 'containers',
};
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
@@ -58,7 +87,7 @@ final class SearchModuleOrderProvider
} }
} }
String _$searchModuleOrderHash() => r'eeea86534497671a12c1383cbf251a8df797c1fc'; String _$searchModuleOrderHash() => r'245c933174b1808b2cb27c4c375fac4edd12d727';
final class SearchModuleOrderFamily extends $Family final class SearchModuleOrderFamily extends $Family
with with
@@ -31,10 +31,13 @@ class HistoryHighlightsSection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final highlightsAsync = ref.watch(searchEmptyHistoryHighlightsProvider()); final highlights = ref.watch(
final highlights = highlightsAsync.value ?? []; searchEmptyHistoryHighlightsProvider().select(
(value) => value.value ?? [],
),
);
if (highlightsAsync.hasValue && highlights.isEmpty) { if (highlights.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink()); return const SliverToBoxAdapter(child: SizedBox.shrink());
} }
@@ -37,10 +37,13 @@ class RecentFeedArticlesSection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final articlesAsync = ref.watch(searchEmptyRecentFeedArticlesProvider()); final articles = ref.watch(
final articles = articlesAsync.value ?? []; searchEmptyRecentFeedArticlesProvider().select(
(value) => value.value ?? [],
),
);
if (articlesAsync.hasValue && articles.isEmpty) { if (articles.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink()); return const SliverToBoxAdapter(child: SizedBox.shrink());
} }
@@ -31,10 +31,11 @@ class RecentHistorySection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final historyAsync = ref.watch(searchEmptyRecentHistoryProvider()); final visits = ref.watch(
final visits = historyAsync.value ?? []; searchEmptyRecentHistoryProvider().select((value) => value.value ?? []),
);
if (historyAsync.hasValue && visits.isEmpty) { if (visits.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink()); return const SliverToBoxAdapter(child: SizedBox.shrink());
} }
@@ -73,12 +73,13 @@ class TopSitesSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final topSitesAsync = ref.watch( final topSites = ref.watch(
topSiteListProvider(limit: _topSitesMaxLimit), topSiteListProvider(
limit: _topSitesMaxLimit,
).select((value) => value.value ?? []),
); );
final topSites = topSitesAsync.value ?? [];
if (topSitesAsync.hasValue && topSites.isEmpty) { if (topSites.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink()); return const SliverToBoxAdapter(child: SizedBox.shrink());
} }
@@ -335,7 +336,7 @@ class _ReorderableTopSitesGrid extends HookConsumerWidget {
} }
} }
class _TopSiteGridTile extends StatelessWidget { class _TopSiteGridTile extends StatefulWidget {
final TopSiteItem item; final TopSiteItem item;
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback? onPin; final VoidCallback? onPin;
@@ -355,19 +356,44 @@ class _TopSiteGridTile extends StatelessWidget {
static const _iconSize = 40.0; static const _iconSize = 40.0;
static const _borderRadius = BorderRadius.all(Radius.circular(12.0)); static const _borderRadius = BorderRadius.all(Radius.circular(12.0));
@override
State<_TopSiteGridTile> createState() => _TopSiteGridTileState();
}
class _TopSiteGridTileState extends State<_TopSiteGridTile> {
final _menuController = MenuController();
bool get _hasMenu =>
(widget.item.isPersisted &&
(widget.onEdit != null || widget.onRemove != null)) ||
(!widget.item.isPersisted && widget.onPin != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return Material( return MenuAnchor(
controller: _menuController,
menuChildren: [
if (!widget.item.isPersisted && widget.onPin != null)
MenuItemButton(onPressed: widget.onPin, child: const Text('Pin')),
if (widget.item.isPersisted && widget.onEdit != null)
MenuItemButton(onPressed: widget.onEdit, child: const Text('Edit')),
if (widget.item.isPersisted && widget.onRemove != null)
MenuItemButton(
onPressed: widget.onRemove,
child: const Text('Remove'),
),
],
child: Material(
color: colorScheme.surfaceContainerHigh, color: colorScheme.surfaceContainerHigh,
borderRadius: _borderRadius, borderRadius: _TopSiteGridTile._borderRadius,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
borderRadius: _borderRadius, borderRadius: _TopSiteGridTile._borderRadius,
onTap: onTap, onTap: widget.onTap,
onLongPress: _hasMenu ? () => _showContextMenu(context) : null, onLongPress: _hasMenu ? () => _menuController.open() : null,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -391,8 +417,11 @@ class _TopSiteGridTile extends StatelessWidget {
final minTextHeight = lineHeight + textHeightPadding; final minTextHeight = lineHeight + textHeightPadding;
final maxTextHeight = final maxTextHeight =
lineHeight * textLines + textHeightPadding; lineHeight * textLines + textHeightPadding;
final iconSize = (constraints.maxHeight - minTextHeight - gap) final iconSize =
.clamp(minIconSize, _iconSize); (constraints.maxHeight - minTextHeight - gap).clamp(
minIconSize,
_TopSiteGridTile._iconSize,
);
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -406,7 +435,9 @@ class _TopSiteGridTile extends StatelessWidget {
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8.0), Radius.circular(8.0),
), ),
child: UrlIcon([item.url], iconSize: iconSize), child: UrlIcon([
widget.item.url,
], iconSize: iconSize),
), ),
), ),
), ),
@@ -414,9 +445,11 @@ class _TopSiteGridTile extends StatelessWidget {
const SizedBox(height: gap), const SizedBox(height: gap),
Flexible( Flexible(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxTextHeight), constraints: BoxConstraints(
maxHeight: maxTextHeight,
),
child: Text( child: Text(
item.title, widget.item.title,
maxLines: textLines, maxLines: textLines,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -429,14 +462,16 @@ class _TopSiteGridTile extends StatelessWidget {
}, },
), ),
), ),
if (item.source == TopSiteSource.pinned) if (widget.item.source == TopSiteSource.pinned)
Positioned( Positioned(
top: 4, top: 4,
right: 4, right: 4,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primaryContainer, color: colorScheme.primaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(3.0), padding: const EdgeInsets.all(3.0),
@@ -448,7 +483,7 @@ class _TopSiteGridTile extends StatelessWidget {
), ),
), ),
), ),
if (showDragHandle) if (widget.showDragHandle)
Positioned( Positioned(
top: 2, top: 2,
right: 2, right: 2,
@@ -461,46 +496,8 @@ class _TopSiteGridTile extends StatelessWidget {
], ],
), ),
), ),
);
}
bool get _hasMenu =>
(item.isPersisted && (onEdit != null || onRemove != null)) ||
(!item.isPersisted && onPin != null);
Future<void> _showContextMenu(BuildContext context) async {
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final value = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy + size.height,
position.dx + size.width,
position.dy + size.height,
), ),
items: [
if (!item.isPersisted && onPin != null)
const PopupMenuItem(value: 'pin', child: Text('Pin')),
if (item.isPersisted && onEdit != null)
const PopupMenuItem(value: 'edit', child: Text('Edit')),
if (item.isPersisted && onRemove != null)
const PopupMenuItem(value: 'remove', child: Text('Remove')),
],
); );
switch (value) {
case 'pin':
onPin?.call();
case 'edit':
onEdit?.call();
case 'remove':
onRemove?.call();
default:
break;
}
} }
} }
@@ -41,7 +41,7 @@ final class TopSiteRepositoryProvider
} }
} }
String _$topSiteRepositoryHash() => r'8aa231fadabe1e115d92cd37e032f27ba8da55e6'; String _$topSiteRepositoryHash() => r'6e954c84ed5916ac0b5a9e251d43cbe1e9ed15c3';
abstract class _$TopSiteRepository extends $Notifier<void> { abstract class _$TopSiteRepository extends $Notifier<void> {
void build(); void build();