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,153 +356,149 @@ 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(
color: colorScheme.surfaceContainerHigh, controller: _menuController,
borderRadius: _borderRadius, menuChildren: [
clipBehavior: Clip.antiAlias, if (!widget.item.isPersisted && widget.onPin != null)
child: InkWell( MenuItemButton(onPressed: widget.onPin, child: const Text('Pin')),
borderRadius: _borderRadius, if (widget.item.isPersisted && widget.onEdit != null)
onTap: onTap, MenuItemButton(onPressed: widget.onEdit, child: const Text('Edit')),
onLongPress: _hasMenu ? () => _showContextMenu(context) : null, if (widget.item.isPersisted && widget.onRemove != null)
child: Stack( MenuItemButton(
fit: StackFit.expand, onPressed: widget.onRemove,
children: [ child: const Text('Remove'),
Padding( ),
padding: const EdgeInsets.symmetric( ],
horizontal: 8.0, child: Material(
vertical: 10.0, color: colorScheme.surfaceContainerHigh,
), borderRadius: _TopSiteGridTile._borderRadius,
child: LayoutBuilder( clipBehavior: Clip.antiAlias,
builder: (context, constraints) { child: InkWell(
final titleStyle = textTheme.bodySmall?.copyWith( borderRadius: _TopSiteGridTile._borderRadius,
color: colorScheme.onSurface, onTap: widget.onTap,
); onLongPress: _hasMenu ? () => _menuController.open() : null,
final lineHeight = child: Stack(
(titleStyle?.fontSize ?? 12.0) * fit: StackFit.expand,
(titleStyle?.height ?? 1.2); children: [
const textLines = 2; Padding(
const gap = 6.0; padding: const EdgeInsets.symmetric(
const minIconSize = 18.0; horizontal: 8.0,
const textHeightPadding = 16.0; vertical: 10.0,
final minTextHeight = lineHeight + textHeightPadding; ),
final maxTextHeight = child: LayoutBuilder(
lineHeight * textLines + textHeightPadding; builder: (context, constraints) {
final iconSize = (constraints.maxHeight - minTextHeight - gap) final titleStyle = textTheme.bodySmall?.copyWith(
.clamp(minIconSize, _iconSize); color: colorScheme.onSurface,
);
final lineHeight =
(titleStyle?.fontSize ?? 12.0) *
(titleStyle?.height ?? 1.2);
const textLines = 2;
const gap = 6.0;
const minIconSize = 18.0;
const textHeightPadding = 16.0;
final minTextHeight = lineHeight + textHeightPadding;
final maxTextHeight =
lineHeight * textLines + textHeightPadding;
final iconSize =
(constraints.maxHeight - minTextHeight - gap).clamp(
minIconSize,
_TopSiteGridTile._iconSize,
);
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Align( Align(
child: SizedBox.square( child: SizedBox.square(
dimension: iconSize, dimension: iconSize,
child: RepaintBoundary( child: RepaintBoundary(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8.0), Radius.circular(8.0),
),
child: UrlIcon([
widget.item.url,
], iconSize: iconSize),
), ),
child: UrlIcon([item.url], iconSize: iconSize),
), ),
), ),
), ),
), const SizedBox(height: gap),
const SizedBox(height: gap), Flexible(
Flexible( child: ConstrainedBox(
child: ConstrainedBox( constraints: BoxConstraints(
constraints: BoxConstraints(maxHeight: maxTextHeight), maxHeight: maxTextHeight,
child: Text( ),
item.title, child: Text(
maxLines: textLines, widget.item.title,
overflow: TextOverflow.ellipsis, maxLines: textLines,
textAlign: TextAlign.center, overflow: TextOverflow.ellipsis,
style: titleStyle, textAlign: TextAlign.center,
style: titleStyle,
),
), ),
), ),
), ],
], );
); },
}, ),
), ),
), if (widget.item.source == TopSiteSource.pinned)
if (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(
borderRadius: const BorderRadius.all(Radius.circular(10.0)), Radius.circular(10.0),
), ),
child: Padding( ),
padding: const EdgeInsets.all(3.0), child: Padding(
child: Icon( padding: const EdgeInsets.all(3.0),
Icons.push_pin, child: Icon(
size: 12, Icons.push_pin,
color: colorScheme.onPrimaryContainer, size: 12,
color: colorScheme.onPrimaryContainer,
),
), ),
), ),
), ),
), if (widget.showDragHandle)
if (showDragHandle) Positioned(
Positioned( top: 2,
top: 2, right: 2,
right: 2, child: Icon(
child: Icon( Icons.drag_indicator,
Icons.drag_indicator, size: 16,
size: 16, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ),
), ),
), ],
], ),
), ),
), ),
); );
} }
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;
}
}
} }
Future<void> _pinItem( Future<void> _pinItem(
@@ -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();