improve tab preview rebuilding behavior

This commit is contained in:
Fabian Freund
2025-08-24 07:52:42 +02:00
parent ee28567b2e
commit 8a7ab7640a
5 changed files with 60 additions and 44 deletions
@@ -183,6 +183,7 @@ class TabTreeDialog extends HookConsumerWidget {
return SizedBox.fromSize( return SizedBox.fromSize(
size: childSize, size: childSize,
child: SingleTabPreview( child: SingleTabPreview(
key: ValueKey(id),
tabId: id, tabId: id,
activeTabId: selectedTabId, activeTabId: selectedTabId,
onClose: () { onClose: () {
@@ -63,7 +63,7 @@ class AppBarTitle extends HookConsumerWidget {
return Row( return Row(
children: [ children: [
TabIcon(state: tabState), TabIcon(tabState: tabState),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
@@ -77,11 +77,13 @@ class _TabDraggable extends HookConsumerWidget {
final tab = (suggestedContainerId != null) final tab = (suggestedContainerId != null)
? SuggestedSingleTabPreview( ? SuggestedSingleTabPreview(
key: ValueKey(entity.tabId),
tabId: entity.tabId, tabId: entity.tabId,
activeTabId: activeTab, activeTabId: activeTab,
containerId: suggestedContainerId!, containerId: suggestedContainerId!,
) )
: SingleTabPreview( : SingleTabPreview(
key: ValueKey(entity.tabId),
tabId: entity.tabId, tabId: entity.tabId,
activeTabId: activeTab, activeTabId: activeTab,
onClose: onClose, onClose: onClose,
@@ -27,25 +27,25 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/presentation/hooks/cached_future.dart'; import 'package:weblibre/presentation/hooks/cached_future.dart';
class TabIcon extends HookConsumerWidget { class TabIcon extends HookConsumerWidget {
final TabState state; final TabState tabState;
final double iconSize; final double iconSize;
const TabIcon({super.key, required this.state, this.iconSize = 16}); const TabIcon({super.key, required this.tabState, this.iconSize = 16});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final icon = useCachedFuture(() async { final icon = useCachedFuture(() async {
if (state.icon != null) { if (tabState.icon != null) {
return state.icon!.value; return tabState.icon!.value;
} }
final icon = await ref final icon = await ref
.read(genericWebsiteServiceProvider.notifier) .read(genericWebsiteServiceProvider.notifier)
.getCachedIcon(state.url); .getCachedIcon(tabState.url);
return icon?.image.value; return icon?.image.value;
}, [state.icon, state.url]); }, [tabState.icon, tabState.url]);
return Skeletonizer( return Skeletonizer(
enabled: icon.connectionState != ConnectionState.done, enabled: icon.connectionState != ConnectionState.done,
@@ -26,7 +26,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
@@ -104,8 +103,8 @@ class TabMiniPreview extends HookConsumerWidget {
} }
} }
class TabPreview extends HookWidget { class TabPreview extends HookConsumerWidget {
final TabState tab; final String tabId;
final bool isActive; final bool isActive;
final VoidCallback? onTap; final VoidCallback? onTap;
@@ -117,7 +116,7 @@ class TabPreview extends HookWidget {
final Widget? trailingChild; final Widget? trailingChild;
const TabPreview({ const TabPreview({
required this.tab, required this.tabId,
required this.isActive, required this.isActive,
this.onTap, this.onTap,
this.onDoubleTap, this.onDoubleTap,
@@ -127,13 +126,19 @@ class TabPreview extends HookWidget {
this.trailingChild, this.trailingChild,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(tabId));
if (tabState == null) {
return const SizedBox.shrink();
}
final extendedDeleteMenuController = useMenuController(); final extendedDeleteMenuController = useMenuController();
return TabContainer( return TabContainer(
isActive: isActive, isActive: isActive,
isPrivate: tab.isPrivate, isPrivate: tabState.isPrivate,
child: InkWell( child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14.0)), borderRadius: const BorderRadius.all(Radius.circular(14.0)),
onTap: onTap, onTap: onTap,
@@ -148,9 +153,9 @@ class TabPreview extends HookWidget {
padding: const EdgeInsets.only(left: 6.0, top: 2.0), padding: const EdgeInsets.only(left: 6.0, top: 2.0),
child: Text( child: Text(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
tab.title, tabState.title,
maxLines: 2, maxLines: 2,
style: tab.isPrivate style: tabState.isPrivate
? const TextStyle(color: Colors.white) ? const TextStyle(color: Colors.white)
: null, : null,
), ),
@@ -164,13 +169,13 @@ class TabPreview extends HookWidget {
}, },
menuChildren: [ menuChildren: [
?onDeleteAll.mapNotNull( ?onDeleteAll.mapNotNull(
(p0) => MenuItemButton( (value) => MenuItemButton(
onPressed: () { onPressed: () {
p0(tab.url.host); value(tabState.url.host);
}, },
leadingIcon: const Icon(MdiIcons.closeBoxMultiple), leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
child: Text('Close all from ${tab.url.host}'), child: Text('Close all from ${tabState.url.host}'),
), ),
), ),
], ],
@@ -196,17 +201,17 @@ class TabPreview extends HookWidget {
Row( Row(
children: [ children: [
const SizedBox(width: 6.0), const SizedBox(width: 6.0),
TabIcon(state: tab), TabIcon(tabState: tabState),
const SizedBox(width: 6.0), const SizedBox(width: 6.0),
Expanded( Expanded(
child: Text( child: Text(
tab.url.authority, tabState.url.authority,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tab.isPrivate ? Colors.white : null, color: tabState.isPrivate ? Colors.white : null,
), ),
), ),
), ),
if (tab.isPrivate) ...[ if (tabState.isPrivate) ...[
const SizedBox(width: 6.0), const SizedBox(width: 6.0),
const SizedBox( const SizedBox(
height: 16, height: 16,
@@ -229,7 +234,7 @@ class TabPreview extends HookWidget {
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
if (tab.thumbnail != null) if (tabState.thumbnail != null)
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -240,7 +245,7 @@ class TabPreview extends HookWidget {
width: double.infinity, width: double.infinity,
child: RepaintBoundary( child: RepaintBoundary(
child: RawImage( child: RawImage(
image: tab.thumbnail!.value, image: tabState.thumbnail!.value,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
), ),
), ),
@@ -262,19 +267,22 @@ class SingleTabPreview extends HookConsumerWidget {
final void Function() onClose; final void Function() onClose;
SingleTabPreview({ const SingleTabPreview({
required this.tabId, required this.tabId,
required this.activeTabId, required this.activeTabId,
required this.onClose, required this.onClose,
required this.sourceSearchQuery, required this.sourceSearchQuery,
this.deleteThreshold = 100, this.deleteThreshold = 100,
}) : super(key: ValueKey(tabId)); super.key,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tab = ref.watch(tabStateProvider(tabId)); final hasTabState = ref.watch(
tabStateProvider(tabId).select((value) => value != null),
);
if (tab == null) { if (!hasTabState) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -296,7 +304,7 @@ class SingleTabPreview extends HookConsumerWidget {
}, },
onHorizontalDragEnd: (details) async { onHorizontalDragEnd: (details) async {
if (draggedDistance.value >= deleteThreshold) { if (draggedDistance.value >= deleteThreshold) {
await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id); await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
} }
draggedDistance.value = 0.0; draggedDistance.value = 0.0;
@@ -304,13 +312,13 @@ class SingleTabPreview extends HookConsumerWidget {
child: Opacity( child: Opacity(
opacity: 1.0 - draggedDistance.value / deleteThreshold, opacity: 1.0 - draggedDistance.value / deleteThreshold,
child: TabPreview( child: TabPreview(
tab: tab, tabId: tabId,
isActive: tabId == activeTabId, isActive: tabId == activeTabId,
onTap: () async { onTap: () async {
if (tabId != activeTabId) { if (tabId != activeTabId) {
//Close first to avoid rebuilds //Close first to avoid rebuilds
onClose(); onClose();
await ref.read(tabRepositoryProvider.notifier).selectTab(tab.id); await ref.read(tabRepositoryProvider.notifier).selectTab(tabId);
if (sourceSearchQuery.isNotEmpty && if (sourceSearchQuery.isNotEmpty &&
ref.read(findInPageControllerProvider(tabId)) == ref.read(findInPageControllerProvider(tabId)) ==
FindInPageState.hidden()) { FindInPageState.hidden()) {
@@ -325,7 +333,7 @@ class SingleTabPreview extends HookConsumerWidget {
onDeleteAll: (host) async { onDeleteAll: (host) async {
final containerId = await ref final containerId = await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.getContainerTabId(tab.id); .getContainerTabId(tabId);
await ref await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
@@ -341,7 +349,7 @@ class SingleTabPreview extends HookConsumerWidget {
// ); // );
// }, // },
onDelete: () async { onDelete: () async {
await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id); await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
}, },
), ),
), ),
@@ -355,24 +363,27 @@ class SuggestedSingleTabPreview extends HookConsumerWidget {
final String? activeTabId; final String? activeTabId;
SuggestedSingleTabPreview({ const SuggestedSingleTabPreview({
required this.tabId, required this.tabId,
required this.containerId, required this.containerId,
required this.activeTabId, required this.activeTabId,
}) : super(key: ValueKey(tabId)); super.key,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tab = ref.watch(tabStateProvider(tabId)); final hasTabState = ref.watch(
tabStateProvider(tabId).select((value) => value != null),
);
if (tab == null) { if (!hasTabState) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Opacity( return Opacity(
opacity: 0.5, opacity: 0.5,
child: TabPreview( child: TabPreview(
tab: tab, tabId: tabId,
isActive: tabId == activeTabId, isActive: tabId == activeTabId,
onTap: () async { onTap: () async {
final containerData = await ref final containerData = await ref
@@ -424,9 +435,11 @@ class TabTreePreview extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tab = ref.watch(tabStateProvider(entity.tabId)); final isTabPrivate = ref.watch(
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
);
if (tab == null) { if (isTabPrivate != null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -440,7 +453,7 @@ class TabTreePreview extends HookConsumerWidget {
stackCount, stackCount,
TabContainer( TabContainer(
isActive: entity.tabId == activeTabId, isActive: entity.tabId == activeTabId,
isPrivate: tab.isPrivate, isPrivate: isTabPrivate == true,
), ),
), ),
_addPadding( _addPadding(
@@ -454,7 +467,7 @@ class TabTreePreview extends HookConsumerWidget {
backgroundColor: Theme.of(context).colorScheme.primaryContainer, backgroundColor: Theme.of(context).colorScheme.primaryContainer,
textColor: Theme.of(context).colorScheme.onPrimaryContainer, textColor: Theme.of(context).colorScheme.onPrimaryContainer,
child: TabPreview( child: TabPreview(
tab: tab, tabId: entity.tabId,
isActive: entity.tabId == activeTabId, isActive: entity.tabId == activeTabId,
onLongPress: () async { onLongPress: () async {
if (entity.tabId != activeTabId) { if (entity.tabId != activeTabId) {
@@ -462,7 +475,7 @@ class TabTreePreview extends HookConsumerWidget {
onClose(); onClose();
await ref await ref
.read(tabRepositoryProvider.notifier) .read(tabRepositoryProvider.notifier)
.selectTab(tab.id); .selectTab(entity.tabId);
} else { } else {
onClose(); onClose();
} }
@@ -475,7 +488,7 @@ class TabTreePreview extends HookConsumerWidget {
onClose(); onClose();
await ref await ref
.read(tabRepositoryProvider.notifier) .read(tabRepositoryProvider.notifier)
.selectTab(tab.id); .selectTab(entity.tabId);
} }
}, },
// onDeleteAll: (host) async { // onDeleteAll: (host) async {