ui refactorings
This commit is contained in:
+35
-26
@@ -950,7 +950,11 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
),
|
||||
)
|
||||
: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
// _TabBar is passed via `child` so it is built once and
|
||||
// reused across toolbar show/hide toggles; only the
|
||||
// `visible` flag fed to _AnimatedToolbar depends on the
|
||||
// watched provider.
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
@@ -961,21 +965,21 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
return _AnimatedToolbar(
|
||||
position: TabBarPosition.bottom,
|
||||
visible: visible,
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.bottom,
|
||||
showMainToolbar:
|
||||
tabBarPosition == TabBarPosition.bottom,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar:
|
||||
displayQuickTabSwitcherBar,
|
||||
isSmallWebMode: false,
|
||||
pointerMoveEvents:
|
||||
tabBarPosition == TabBarPosition.bottom
|
||||
? pointerMoveEventsController.stream
|
||||
: null,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.bottom,
|
||||
showMainToolbar:
|
||||
tabBarPosition == TabBarPosition.bottom,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
||||
isSmallWebMode: false,
|
||||
pointerMoveEvents:
|
||||
tabBarPosition == TabBarPosition.bottom
|
||||
? pointerMoveEventsController.stream
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -986,7 +990,11 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
// _TabBar is passed via `child` so it is built once and
|
||||
// reused across toolbar show/hide toggles; only the
|
||||
// `visible` flag fed to _AnimatedToolbar depends on the
|
||||
// watched provider.
|
||||
builder: (context, ref, child) {
|
||||
final toolbarState = ref.watch(
|
||||
toolbarVisibilityControllerProvider(selectedTabId),
|
||||
);
|
||||
@@ -997,19 +1005,20 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
return _AnimatedToolbar(
|
||||
position: TabBarPosition.top,
|
||||
visible: visible,
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.top,
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
||||
isSmallWebMode: isSmallWebActive,
|
||||
enableGestures: !isSmallWebActive,
|
||||
pointerMoveEvents: isSmallWebActive
|
||||
? null
|
||||
: pointerMoveEventsController.stream,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: _TabBar(
|
||||
tabBarPosition: TabBarPosition.top,
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: showContextualToolbar,
|
||||
showQuickTabSwitcherBar: displayQuickTabSwitcherBar,
|
||||
isSmallWebMode: isSmallWebActive,
|
||||
enableGestures: !isSmallWebActive,
|
||||
pointerMoveEvents: isSmallWebActive
|
||||
? null
|
||||
: pointerMoveEventsController.stream,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
+70
-39
@@ -501,6 +501,57 @@ class QuickTabSwitcherItem with FastEquatable {
|
||||
this.depth = 0,
|
||||
});
|
||||
|
||||
/// Builds a switcher entry for an open tab. [sandboxSourceUri] is the
|
||||
/// canonical source URL when the tab is a sandbox capture (otherwise null),
|
||||
/// so the bar shows the real site instead of the loopback capture URL.
|
||||
factory QuickTabSwitcherItem.tab(
|
||||
TabStateWithContainer state, {
|
||||
required String? selectedTabId,
|
||||
required Set<String> pinnedTabIds,
|
||||
required Map<String, int> tabDepthById,
|
||||
required Uri? sandboxSourceUri,
|
||||
}) {
|
||||
final (tab, container) = state;
|
||||
|
||||
return QuickTabSwitcherItem(
|
||||
color: container?.color,
|
||||
useCustomColor: container?.metadata.useCustomColor ?? false,
|
||||
id: tab.id,
|
||||
isActive: tab.id == selectedTabId,
|
||||
title: sandboxSourceUri != null && tab.title.isEmpty
|
||||
? sandboxSourceUri.authority
|
||||
: tab.titleOrAuthority,
|
||||
tabMode: tab.tabMode,
|
||||
isHistory: false,
|
||||
isPinned: pinnedTabIds.contains(tab.id),
|
||||
isSandbox: sandboxSourceUri != null,
|
||||
depth: tabDepthById[tab.id] ?? 0,
|
||||
url: sandboxSourceUri ?? tab.url,
|
||||
avatar: TabIcon(tabState: tab, iconSize: 20),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a switcher entry for a history suggestion (shown only when there
|
||||
/// are no open tabs in the active mode).
|
||||
factory QuickTabSwitcherItem.history({
|
||||
required String url,
|
||||
required String? title,
|
||||
}) {
|
||||
final parsedUrl = Uri.parse(url);
|
||||
|
||||
return QuickTabSwitcherItem(
|
||||
color: null,
|
||||
id: url,
|
||||
isActive: false,
|
||||
title: title ?? parsedUrl.authority,
|
||||
tabMode: TabMode.regular,
|
||||
isHistory: true,
|
||||
isPinned: false,
|
||||
url: parsedUrl,
|
||||
avatar: UrlIcon([parsedUrl], iconSize: 20),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
color,
|
||||
@@ -587,45 +638,25 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
);
|
||||
final reorderEnabled =
|
||||
effectiveMode == QuickTabSwitcherMode.containerTabs && canManualReorder;
|
||||
final tabItems = tabStates.value.map<QuickTabSwitcherItem>((state) {
|
||||
final sandboxSourceUri = parseSandboxSource(
|
||||
sandboxCaptureMap[state.$1.id],
|
||||
);
|
||||
final displayUrl = sandboxSourceUri ?? state.$1.url;
|
||||
final displayTitle = sandboxSourceUri != null && state.$1.title.isEmpty
|
||||
? sandboxSourceUri.authority
|
||||
: state.$1.titleOrAuthority;
|
||||
return QuickTabSwitcherItem(
|
||||
color: state.$2?.color,
|
||||
useCustomColor: state.$2?.metadata.useCustomColor ?? false,
|
||||
id: state.$1.id,
|
||||
isActive: state.$1.id == selectedTabId,
|
||||
title: displayTitle,
|
||||
tabMode: state.$1.tabMode,
|
||||
isHistory: false,
|
||||
isPinned: pinnedTabIds.contains(state.$1.id),
|
||||
isSandbox: sandboxSourceUri != null,
|
||||
depth: tabDepthById[state.$1.id] ?? 0,
|
||||
url: displayUrl,
|
||||
avatar: TabIcon(tabState: state.$1, iconSize: 20),
|
||||
);
|
||||
}).toList();
|
||||
final historyItems = (historySuggestions ?? []).map<QuickTabSwitcherItem>((
|
||||
state,
|
||||
) {
|
||||
final url = Uri.parse(state.url);
|
||||
return QuickTabSwitcherItem(
|
||||
color: null,
|
||||
id: state.url,
|
||||
isActive: false,
|
||||
title: state.title ?? url.authority,
|
||||
tabMode: TabMode.regular,
|
||||
isHistory: true,
|
||||
isPinned: false,
|
||||
url: url,
|
||||
avatar: UrlIcon([url], iconSize: 20),
|
||||
);
|
||||
}).toList();
|
||||
final tabItems = tabStates.value
|
||||
.map(
|
||||
(state) => QuickTabSwitcherItem.tab(
|
||||
state,
|
||||
selectedTabId: selectedTabId,
|
||||
pinnedTabIds: pinnedTabIds,
|
||||
tabDepthById: tabDepthById,
|
||||
sandboxSourceUri: parseSandboxSource(
|
||||
sandboxCaptureMap[state.$1.id],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final historyItems = (historySuggestions ?? [])
|
||||
.map(
|
||||
(visit) =>
|
||||
QuickTabSwitcherItem.history(url: visit.url, title: visit.title),
|
||||
)
|
||||
.toList();
|
||||
final availableItems = [...tabItems, ...historyItems];
|
||||
|
||||
final activeItem = availableItems.isEmpty
|
||||
|
||||
+140
-132
@@ -192,138 +192,9 @@ class ViewTabTreesWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
|
||||
Expanded(
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(
|
||||
context,
|
||||
);
|
||||
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(
|
||||
containerId: containerId,
|
||||
),
|
||||
groupTrees: true,
|
||||
),
|
||||
);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
filteredTabEntities.value.length,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, filteredTabEntities.value.length]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients || activeTab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index < 0) return;
|
||||
|
||||
final row = index ~/ 2;
|
||||
final tabStart = row * itemSize.height;
|
||||
final viewportDimension =
|
||||
scrollController.position.viewportDimension;
|
||||
|
||||
final targetOffset =
|
||||
(tabStart -
|
||||
viewportDimension / 2 +
|
||||
itemSize.height / 2)
|
||||
.clamp(
|
||||
0.0,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
|
||||
if (targetOffset == scrollController.offset) return;
|
||||
|
||||
if (disableAnimations) {
|
||||
scrollController.jumpTo(targetOffset);
|
||||
} else {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
targetOffset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
final tabs = useMemoized(() {
|
||||
return filteredTabEntities.value
|
||||
.whereType<TabTreeEntity>()
|
||||
.map((entity) {
|
||||
return _TabTreePreview(
|
||||
entity: entity,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
stackPadding: const Offset(8, 8),
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FadingScroll(
|
||||
controller: scrollController,
|
||||
fadingSize: 5,
|
||||
builder: (context, controller) {
|
||||
return GridView.builder(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(bottom: 56),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) => tabs[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _TabTreesGrid(
|
||||
scrollController: scrollController,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -353,3 +224,140 @@ class ViewTabTreesWidget extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollable grid of tab-tree previews.
|
||||
///
|
||||
/// Extracted into its own [HookConsumerWidget] (rather than an inline
|
||||
/// `HookConsumer`) so the heavy `seamlessFilteredTabEntitiesProvider` watch
|
||||
/// and the layout/scroll-sync hook state live in a stable, dedicated element
|
||||
/// instead of an anonymous builder closure. This keeps the surrounding
|
||||
/// [TabViewHeader] and FAB out of this subtree's rebuild scope.
|
||||
class _TabTreesGrid extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabTreesGrid({required this.scrollController, required this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(containerId: containerId),
|
||||
groupTrees: true,
|
||||
),
|
||||
);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(calculatedCount, filteredTabEntities.value.length),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, filteredTabEntities.value.length]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients || activeTab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index < 0) return;
|
||||
|
||||
final row = index ~/ 2;
|
||||
final tabStart = row * itemSize.height;
|
||||
final viewportDimension =
|
||||
scrollController.position.viewportDimension;
|
||||
|
||||
final targetOffset =
|
||||
(tabStart - viewportDimension / 2 + itemSize.height / 2).clamp(
|
||||
0.0,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
|
||||
if (targetOffset == scrollController.offset) return;
|
||||
|
||||
if (disableAnimations) {
|
||||
scrollController.jumpTo(targetOffset);
|
||||
} else {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
targetOffset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
final tabs = useMemoized(() {
|
||||
return filteredTabEntities.value
|
||||
.whereType<TabTreeEntity>()
|
||||
.map((entity) {
|
||||
return _TabTreePreview(
|
||||
entity: entity,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
stackPadding: const Offset(8, 8),
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FadingScroll(
|
||||
controller: scrollController,
|
||||
fadingSize: 5,
|
||||
builder: (context, controller) {
|
||||
return GridView.builder(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(bottom: 56),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) => tabs[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -1142,12 +1142,7 @@ class TabViewHeader extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
if (showContainerUi)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
return _TabFilters(tabsViewMode: tabsViewMode);
|
||||
},
|
||||
),
|
||||
if (showContainerUi) _TabFilters(tabsViewMode: tabsViewMode),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
|
||||
+67
-47
@@ -52,6 +52,24 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
? selectedContainerTabs.value[selectedContainer.value!]
|
||||
: null;
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
selectedContainer.value?.tabIds.length ?? 0,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, selectedContainer.value?.tabIds.length]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Draft Containers')),
|
||||
body: SafeArea(
|
||||
@@ -60,24 +78,6 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
child: suggestionsAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (suggestions) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
selectedContainer.value?.tabIds.length ?? 0,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, selectedContainer.value?.tabIds.length]);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -88,35 +88,10 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
itemId: (container) => container,
|
||||
itemAvatar: (container) =>
|
||||
const Icon(MdiIcons.creation, size: 20),
|
||||
itemLabel: (container) => HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final items = useListenableSelector(
|
||||
selectedContainerTabs,
|
||||
() => selectedContainerTabs.value[container],
|
||||
);
|
||||
|
||||
final topic = items.isNotEmpty
|
||||
? ref.watch(
|
||||
tabsTopicProvider(EquatableValue(items!)),
|
||||
)
|
||||
: AsyncValue.data(container.topic);
|
||||
|
||||
return topic.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (topic) => Text(topic ?? 'Untitled'),
|
||||
error: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed predicting selected tabs topic',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
return Text(container.topic ?? 'Untitled');
|
||||
},
|
||||
loading: () =>
|
||||
const Skeletonizer(child: Text('Untitled')),
|
||||
);
|
||||
},
|
||||
itemLabel: (container) => _SuggestedContainerLabel(
|
||||
key: ValueKey(container),
|
||||
container: container,
|
||||
selectedContainerTabs: selectedContainerTabs,
|
||||
),
|
||||
itemBadgeCount: (container) => container.tabIds.length,
|
||||
availableItems: suggestions!,
|
||||
@@ -237,3 +212,48 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Label for a single suggested-container chip.
|
||||
///
|
||||
/// Extracted into its own [HookConsumerWidget] (rather than an inline
|
||||
/// `HookConsumer` in `itemLabel`) so its hook state is bound to a stable
|
||||
/// element identity per container, and the heavy `tabsTopicProvider` watch
|
||||
/// is scoped to just this chip.
|
||||
class _SuggestedContainerLabel extends HookConsumerWidget {
|
||||
final SuggestedContainer container;
|
||||
final ValueNotifier<Map<SuggestedContainer, Set<String>>>
|
||||
selectedContainerTabs;
|
||||
|
||||
const _SuggestedContainerLabel({
|
||||
required this.container,
|
||||
required this.selectedContainerTabs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final items = useListenableSelector(
|
||||
selectedContainerTabs,
|
||||
() => selectedContainerTabs.value[container],
|
||||
);
|
||||
|
||||
final topic = items.isNotEmpty
|
||||
? ref.watch(tabsTopicProvider(EquatableValue(items!)))
|
||||
: AsyncValue.data(container.topic);
|
||||
|
||||
return topic.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (topic) => Text(topic ?? 'Untitled'),
|
||||
error: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed predicting selected tabs topic',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
return Text(container.topic ?? 'Untitled');
|
||||
},
|
||||
loading: () => const Skeletonizer(child: Text('Untitled')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,11 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
final GlobalKey? activeItemKey;
|
||||
final double? cacheExtent;
|
||||
|
||||
/// Key applied to the underlying scroll view. Supply a [PageStorageKey] to
|
||||
/// preserve the scroll offset across rebuilds/remounts (e.g. when a host
|
||||
/// widget is torn down and recreated by a bottom sheet open/close).
|
||||
final Key? scrollKey;
|
||||
|
||||
final K Function(S item) itemId;
|
||||
final Widget Function(T item) itemLabel;
|
||||
final Widget? Function(T item)? itemAvatar;
|
||||
@@ -187,6 +192,7 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
this.scrollController,
|
||||
this.activeItemKey,
|
||||
this.cacheExtent = 0,
|
||||
this.scrollKey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -296,6 +302,7 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
builder: (context, controller) {
|
||||
if (onReorder == null) {
|
||||
return ListView.builder(
|
||||
key: scrollKey,
|
||||
controller: controller,
|
||||
scrollCacheExtent: cacheExtent.mapNotNull(
|
||||
(extent) => ScrollCacheExtent.pixels(extent),
|
||||
|
||||
Reference in New Issue
Block a user