From b3a3b9930b75dd41997347bebabae718406727ef Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 15 Dec 2025 21:07:32 +0100 Subject: [PATCH] avoid using builders --- .../browser/presentation/screens/browser.dart | 210 ++++----- .../presentation/widgets/view_tabs.dart | 402 +++++++++--------- .../presentation/screens/container_list.dart | 204 +++++---- .../screens/container_selection.dart | 65 ++- .../screens/feed_article_list.dart | 289 ++++++------- 5 files changed, 575 insertions(+), 595 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index d6954cff..4db51593 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -52,6 +52,112 @@ import 'package:weblibre/features/geckoview/features/readerview/presentation/wid import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; +class _TabBar extends HookConsumerWidget { + final ValueNotifier showAppBar; + final ValueNotifier sheetController; + + const _TabBar({required this.showAppBar, required this.sheetController}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabId = ref.watch(selectedTabProvider); + final displayedSheet = ref.watch(bottomSheetControllerProvider); + + final tabInFullScreen = ref.watch( + selectedTabStateProvider.select((value) => value?.isFullScreen ?? false), + ); + + final autoHideTabBar = ref.watch( + generalSettingsWithDefaultsProvider.select( + (value) => value.autoHideTabBar, + ), + ); + + if (!autoHideTabBar) { + return Visibility( + visible: !tabInFullScreen, + child: BrowserBottomAppBar(displayedSheet: displayedSheet), + ); + } + + final appBarVisible = useValueListenable(showAppBar); + final diffAcc = useRef(0.0); + + void resetHiddenState() { + showAppBar.value = true; + diffAcc.value = 0.0; + } + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + resetHiddenState(); + }); + + return null; + }, [tabId]); + + useOnAppLifecycleStateChange((previous, current) { + if (current == AppLifecycleState.resumed) { + resetHiddenState(); + } + }); + + ref.listen(tabStateProvider(tabId).select((value) => value?.isLoading), ( + previous, + next, + ) { + if (next == true) { + resetHiddenState(); + } + }); + + ref.listen(tabStateProvider(tabId).select((value) => value?.historyState), ( + previous, + next, + ) { + if (next != null && previous != null) { + if (previous != next) { + resetHiddenState(); + } + } + }); + + ref.listen(tabScrollYProvider(tabId, const Duration(milliseconds: 50)), ( + previous, + next, + ) { + if (previous?.value != null && next.value != null) { + final diff = previous!.value! - next.value!; + if (diff < 0) { + if (diffAcc.value > 0) { + diffAcc.value = 0.0; + } + + diffAcc.value += diff; + if (diffAcc.value.abs() > kToolbarHeight * 1.5) { + showAppBar.value = false; + } + } else if (diff > 0) { + if (diffAcc.value < 0) { + diffAcc.value = 0.0; + } + + diffAcc.value += diff; + if (diffAcc.value.abs() > (kToolbarHeight / 2)) { + resetHiddenState(); + } + } + } + }); + + return Visibility( + visible: + sheetController.value != null || (!tabInFullScreen && appBarVisible), + child: BrowserBottomAppBar(displayedSheet: displayedSheet), + ); + } +} + class BrowserScreen extends HookConsumerWidget { const BrowserScreen({super.key}); @@ -114,107 +220,9 @@ class BrowserScreen extends HookConsumerWidget { //This causes issues with a non dismissable barrier pushed, we ahve our own barrier and this does seem to have issues when dismissing, so disable it completely return null; }, - bottomNavigationBar: HookConsumer( - builder: (context, ref, child) { - final tabId = ref.watch(selectedTabProvider); - final displayedSheet = ref.watch(bottomSheetControllerProvider); - - final tabInFullScreen = ref.watch( - selectedTabStateProvider.select( - (value) => value?.isFullScreen ?? false, - ), - ); - - final autoHideTabBar = ref.watch( - generalSettingsWithDefaultsProvider.select( - (value) => value.autoHideTabBar, - ), - ); - - if (!autoHideTabBar) { - return Visibility( - visible: !tabInFullScreen, - child: BrowserBottomAppBar(displayedSheet: displayedSheet), - ); - } - - final appBarVisible = useValueListenable(showAppBar); - final diffAcc = useRef(0.0); - - void resetHiddenState() { - showAppBar.value = true; - diffAcc.value = 0.0; - } - - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - resetHiddenState(); - }); - - return null; - }, [tabId]); - - useOnAppLifecycleStateChange((previous, current) { - if (current == AppLifecycleState.resumed) { - resetHiddenState(); - } - }); - - ref.listen( - tabStateProvider(tabId).select((value) => value?.isLoading), - (previous, next) { - if (next == true) { - resetHiddenState(); - } - }, - ); - - ref.listen( - tabStateProvider(tabId).select((value) => value?.historyState), - (previous, next) { - if (next != null && previous != null) { - if (previous != next) { - resetHiddenState(); - } - } - }, - ); - - ref.listen( - tabScrollYProvider(tabId, const Duration(milliseconds: 50)), - (previous, next) { - if (previous?.value != null && next.value != null) { - final diff = previous!.value! - next.value!; - if (diff < 0) { - if (diffAcc.value > 0) { - diffAcc.value = 0.0; - } - - diffAcc.value += diff; - if (diffAcc.value.abs() > kToolbarHeight * 1.5) { - showAppBar.value = false; - } - } else if (diff > 0) { - if (diffAcc.value < 0) { - diffAcc.value = 0.0; - } - - diffAcc.value += diff; - if (diffAcc.value.abs() > (kToolbarHeight / 2)) { - resetHiddenState(); - } - } - } - }, - ); - - return Visibility( - visible: - sheetController.value != null || - (!tabInFullScreen && appBarVisible), - child: BrowserBottomAppBar(displayedSheet: displayedSheet), - ); - }, + bottomNavigationBar: _TabBar( + showAppBar: showAppBar, + sheetController: sheetController, ), body: _Browser( overlayController: overlayController, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/view_tabs.dart index 37ac2e96..baf49091 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/view_tabs.dart @@ -370,6 +370,202 @@ class _TabViewHeader extends HookConsumerWidget { } } +class _TabView extends HookConsumerWidget { + final ScrollController scrollController; + final VoidCallback onClose; + + const _TabView({required this.scrollController, required this.onClose}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final screenWidth = MediaQuery.of(context).size.width; + + 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: false, + ), + ); + + final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider); + + final suggestedTabEntities = tabSuggestionsEnabled + ? ref.watch(suggestedTabEntitiesProvider(containerId)) + : EquatableValue([]); + + final itemCount = + filteredTabEntities.value.length + + //Limit to 3 sugegstions for now + math.min(suggestedTabEntities.value.length, 3); + + 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, itemCount), 2); + }, [screenWidth, itemCount]); + + final itemHeight = useMemoized( + () => calculateItemHeight( + screenWidth: screenWidth, + childAspectRatio: 0.75, + horizontalPadding: 4.0, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: crossAxisCount, + ), + [screenWidth, crossAxisCount], + ); + + final lastScroll = useRef(null); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + if (lastScroll.value != activeTab) { + final index = filteredTabEntities.value.indexWhere( + (entity) => entity.tabId == activeTab, + ); + + if (index > -1) { + final offset = (index ~/ 2) * itemHeight; + + if (offset != scrollController.offset) { + lastScroll.value = activeTab; + + unawaited( + scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ); + } + } + } + } + }); + + return null; + }, [filteredTabEntities, activeTab]); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: FadingScroll( + fadingSize: 5, + controller: scrollController, + builder: (context, controller) { + return ReorderableBuilder.builder( + //Rebuild when cross axis count changes + key: ValueKey(crossAxisCount), + scrollController: controller, + itemCount: itemCount, + onDragStarted: (index) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onReorderPositions: (positions) async { + assert(positions.length == 1, 'Not ready for multiple reorders'); + + final oldIndex = positions.first.oldIndex; + final newIndex = positions.first.newIndex; + + final containerRepository = ref.read( + containerRepositoryProvider.notifier, + ); + + //Suggestions are at the end and not reorderable, so skip + if (oldIndex >= filteredTabEntities.value.length) { + return; + } + + final tabId = filteredTabEntities.value[oldIndex].tabId; + final containerId = await ref + .read(tabDataRepositoryProvider.notifier) + .getContainerTabId(tabId); + + final String key; + if (newIndex <= 0) { + key = await containerRepository.getLeadingOrderKey(containerId); + } else if (newIndex >= filteredTabEntities.value.length - 1) { + key = await containerRepository.getTrailingOrderKey( + containerId, + ); + } else { + if (newIndex < oldIndex) { + key = (await containerRepository.getOrderKeyAfterTab( + filteredTabEntities.value[newIndex - 1].tabId, + containerId, + ))!; + } else { + key = await containerRepository.getOrderKeyBeforeTab( + filteredTabEntities.value[newIndex + 1].tabId, + containerId, + ); + } + } + + await ref + .read(tabDataRepositoryProvider.notifier) + .assignOrderKey(tabId, key); + }, + childBuilder: (itemBuilder) { + return GridView.builder( + controller: controller, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //Sync values for itemHeight calculation _calculateItemHeight + childAspectRatio: 0.75, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: crossAxisCount, + ), + itemCount: itemCount, + itemBuilder: (context, index) { + final Widget tab; + if (index < filteredTabEntities.value.length) { + final entity = filteredTabEntities.value[index]; + tab = CustomDraggable( + key: Key(entity.tabId), + data: TabDragData(entity.tabId), + child: _TabDraggable(entity: entity, onClose: onClose), + ); + } else { + final suggestedIndex = + index - filteredTabEntities.value.length; + final entity = suggestedTabEntities.value[suggestedIndex]; + + tab = CustomDraggable( + key: Key('suggested_${entity.tabId}'), + child: _TabDraggable( + entity: entity, + onClose: onClose, + suggestedContainerId: ref.watch( + selectedContainerProvider, + ), + ), + ); + } + + return itemBuilder(tab, index); + }, + ); + }, + ); + }, + ), + ); + } +} + class ViewTabsWidget extends HookConsumerWidget { final ScrollController scrollController; final DraggableScrollableController? draggableScrollableController; @@ -407,211 +603,7 @@ class ViewTabsWidget extends HookConsumerWidget { _TabViewHeader(onClose: onClose, treeViewEnabled: false), ), ], - body: HookConsumer( - builder: (context, ref, child) { - final screenWidth = MediaQuery.of(context).size.width; - - 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: false, - ), - ); - - final tabSuggestionsEnabled = ref.watch( - tabSuggestionsControllerProvider, - ); - - final suggestedTabEntities = tabSuggestionsEnabled - ? ref.watch(suggestedTabEntitiesProvider(containerId)) - : EquatableValue([]); - - final itemCount = - filteredTabEntities.value.length + - //Limit to 3 sugegstions for now - math.min(suggestedTabEntities.value.length, 3); - - 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, itemCount), 2); - }, [screenWidth, itemCount]); - - final itemHeight = useMemoized( - () => calculateItemHeight( - screenWidth: screenWidth, - childAspectRatio: 0.75, - horizontalPadding: 4.0, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: crossAxisCount, - ), - [screenWidth, crossAxisCount], - ); - - final lastScroll = useRef(null); - - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - if (lastScroll.value != activeTab) { - final index = filteredTabEntities.value.indexWhere( - (entity) => entity.tabId == activeTab, - ); - - if (index > -1) { - final offset = (index ~/ 2) * itemHeight; - - if (offset != scrollController.offset) { - lastScroll.value = activeTab; - - unawaited( - scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ), - ); - } - } - } - } - }); - - return null; - }, [filteredTabEntities, activeTab]); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FadingScroll( - fadingSize: 5, - controller: scrollController, - builder: (context, controller) { - return ReorderableBuilder.builder( - //Rebuild when cross axis count changes - key: ValueKey(crossAxisCount), - scrollController: controller, - itemCount: itemCount, - onDragStarted: (index) { - ref.read(willAcceptDropProvider.notifier).clear(); - }, - onReorderPositions: (positions) async { - assert( - positions.length == 1, - 'Not ready for multiple reorders', - ); - - final oldIndex = positions.first.oldIndex; - final newIndex = positions.first.newIndex; - - final containerRepository = ref.read( - containerRepositoryProvider.notifier, - ); - - //Suggestions are at the end and not reorderable, so skip - if (oldIndex >= filteredTabEntities.value.length) { - return; - } - - final tabId = filteredTabEntities.value[oldIndex].tabId; - final containerId = await ref - .read(tabDataRepositoryProvider.notifier) - .getContainerTabId(tabId); - - final String key; - if (newIndex <= 0) { - key = await containerRepository.getLeadingOrderKey( - containerId, - ); - } else if (newIndex >= - filteredTabEntities.value.length - 1) { - key = await containerRepository.getTrailingOrderKey( - containerId, - ); - } else { - if (newIndex < oldIndex) { - key = (await containerRepository - .getOrderKeyAfterTab( - filteredTabEntities.value[newIndex - 1].tabId, - containerId, - ))!; - } else { - key = await containerRepository - .getOrderKeyBeforeTab( - filteredTabEntities.value[newIndex + 1].tabId, - containerId, - ); - } - } - - await ref - .read(tabDataRepositoryProvider.notifier) - .assignOrderKey(tabId, key); - }, - childBuilder: (itemBuilder) { - return GridView.builder( - controller: controller, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - //Sync values for itemHeight calculation _calculateItemHeight - childAspectRatio: 0.75, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: crossAxisCount, - ), - itemCount: itemCount, - itemBuilder: (context, index) { - final Widget tab; - if (index < filteredTabEntities.value.length) { - final entity = filteredTabEntities.value[index]; - tab = CustomDraggable( - key: Key(entity.tabId), - data: TabDragData(entity.tabId), - child: _TabDraggable( - entity: entity, - onClose: onClose, - ), - ); - } else { - final suggestedIndex = - index - filteredTabEntities.value.length; - final entity = - suggestedTabEntities.value[suggestedIndex]; - - tab = CustomDraggable( - key: Key('suggested_${entity.tabId}'), - child: _TabDraggable( - entity: entity, - onClose: onClose, - suggestedContainerId: ref.watch( - selectedContainerProvider, - ), - ), - ); - } - - return itemBuilder(tab, index); - }, - ); - }, - ); - }, - ), - ); - }, - ), + body: _TabView(scrollController: scrollController, onClose: onClose), ), if (showNewTabFab) Padding( diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart index 6937cb3e..92b13b8a 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart @@ -38,120 +38,114 @@ class ContainerListScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final containersAsync = ref.watch(watchContainersWithCountProvider); + final selectedContainer = ref.watch(selectedContainerProvider); + return Scaffold( appBar: AppBar(title: const Text('Containers')), - body: HookConsumer( - builder: (context, ref, child) { - final containersAsync = ref.watch(watchContainersWithCountProvider); - final selectedContainer = ref.watch(selectedContainerProvider); + body: Skeletonizer( + enabled: containersAsync.isLoading, + child: containersAsync.when( + skipLoadingOnReload: true, + data: (containers) => FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: containers.length, + itemBuilder: (context, index) { + final container = containers[index]; + return Slidable( + key: ValueKey(container.id), + startActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + if (container.id != selectedContainer) + SlidableAction( + onPressed: (context) async { + final result = await ref + .read(selectedContainerProvider.notifier) + .setContainerId(container.id); - return Skeletonizer( - enabled: containersAsync.isLoading, - child: containersAsync.when( - skipLoadingOnReload: true, - data: (containers) => FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return ListView.builder( - controller: controller, - itemCount: containers.length, - itemBuilder: (context, index) { - final container = containers[index]; - return Slidable( - key: ValueKey(container.id), - startActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - if (container.id != selectedContainer) - SlidableAction( - onPressed: (context) async { - final result = await ref - .read(selectedContainerProvider.notifier) - .setContainerId(container.id); - - if (context.mounted && - result == - SetContainerResult.successHasProxy) { - await ref - .read( - startProxyControllerProvider.notifier, - ) - .maybeStartProxy(context); - } - }, - foregroundColor: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - icon: Icons.check, - label: 'Select', - ) - else - SlidableAction( - onPressed: (context) { - ref - .read(selectedContainerProvider.notifier) - .clearContainer(); - }, - foregroundColor: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - icon: Icons.close, - label: 'Unselect', - ), - ], - ), - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - SlidableAction( - onPressed: (context) async { + if (context.mounted && + result == + SetContainerResult.successHasProxy) { await ref - .read(containerRepositoryProvider.notifier) - .deleteContainer(container.id); - }, - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - foregroundColor: Theme.of( - context, - ).colorScheme.onErrorContainer, - icon: Icons.delete, - label: 'Delete', - ), - ], - ), - child: ContainerListTile( - container, - isSelected: container.id == selectedContainer, - onTap: () async { - await ContainerEditRoute( - containerData: jsonEncode(container.toJson()), - ).push(context); + .read(startProxyControllerProvider.notifier) + .maybeStartProxy(context); + } + }, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, + icon: Icons.check, + label: 'Select', + ) + else + SlidableAction( + onPressed: (context) { + ref + .read(selectedContainerProvider.notifier) + .clearContainer(); + }, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, + icon: Icons.close, + label: 'Unselect', + ), + ], + ), + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (context) async { + await ref + .read(containerRepositoryProvider.notifier) + .deleteContainer(container.id); }, + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, + foregroundColor: Theme.of( + context, + ).colorScheme.onErrorContainer, + icon: Icons.delete, + label: 'Delete', ), - ); - }, + ], + ), + child: ContainerListTile( + container, + isSelected: container.id == selectedContainer, + onTap: () async { + await ContainerEditRoute( + containerData: jsonEncode(container.toJson()), + ).push(context); + }, + ), ); }, - ), - error: (error, stackTrace) => SizedBox.shrink(), - loading: () => ListView.builder( - itemCount: 3, - itemBuilder: (context, index) => ContainerListTile( - ContainerData(id: 'null', color: Colors.transparent), - onTap: null, - isSelected: false, - ), - ), + ); + }, + ), + error: (error, stackTrace) => SizedBox.shrink(), + loading: () => ListView.builder( + itemCount: 3, + itemBuilder: (context, index) => ContainerListTile( + ContainerData(id: 'null', color: Colors.transparent), + onTap: null, + isSelected: false, ), - ); - }, + ), + ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart index b96f6309..25e2b6cc 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_selection.dart @@ -36,47 +36,42 @@ class ContainerSelectionScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final containersAsync = ref.watch(watchContainersWithCountProvider); return Scaffold( appBar: AppBar(title: const Text('Select Container')), - body: HookConsumer( - builder: (context, ref, child) { - final containersAsync = ref.watch(watchContainersWithCountProvider); - - return Skeletonizer( - enabled: containersAsync.isLoading, - child: containersAsync.when( - skipLoadingOnReload: true, - data: (containers) => FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return ListView.builder( - controller: controller, - itemCount: containers.length, - itemBuilder: (context, index) { - final container = containers[index]; - return ContainerListTile( - container, - isSelected: false, - onTap: () { - context.pop(container.id); - }, - ); + body: Skeletonizer( + enabled: containersAsync.isLoading, + child: containersAsync.when( + skipLoadingOnReload: true, + data: (containers) => FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: containers.length, + itemBuilder: (context, index) { + final container = containers[index]; + return ContainerListTile( + container, + isSelected: false, + onTap: () { + context.pop(container.id); }, ); }, - ), - error: (error, stackTrace) => SizedBox.shrink(), - loading: () => ListView.builder( - itemCount: 3, - itemBuilder: (context, index) => ContainerListTile( - ContainerData(id: 'null', color: Colors.transparent), - isSelected: false, - onTap: null, - ), - ), + ); + }, + ), + error: (error, stackTrace) => SizedBox.shrink(), + loading: () => ListView.builder( + itemCount: 3, + itemBuilder: (context, index) => ContainerListTile( + ContainerData(id: 'null', color: Colors.transparent), + isSelected: false, + onTap: null, ), - ); - }, + ), + ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { diff --git a/app/lib/features/web_feed/presentation/screens/feed_article_list.dart b/app/lib/features/web_feed/presentation/screens/feed_article_list.dart index e5ed6276..d310ece5 100644 --- a/app/lib/features/web_feed/presentation/screens/feed_article_list.dart +++ b/app/lib/features/web_feed/presentation/screens/feed_article_list.dart @@ -37,170 +37,161 @@ class FeedArticleListScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final tags = ref.watch(articleFilterProvider); + final articlesAsync = ref.watch( + // ignore: provider_parameters + filteredArticleListProvider(feedId), + ); + + final feedTitle = ref.watch( + feedDataProvider( + feedId, + ).select((value) => value.value?.title.whenNotEmpty), + ); + + final focusNode = useFocusNode(); + final searchTextController = useTextEditingController(); + + final hasText = useListenableSelector( + searchTextController, + () => searchTextController.text.isNotEmpty, + ); + + useListenableCallback(searchTextController, () { + ref + .read(filteredArticleListProvider(feedId).notifier) + .search(searchTextController.text); + }); + + final bottomHeight = useMemoized(() { + var height = 56.0 + 4.0; + + if (tags.isNotEmpty) { + height += 48; + } + + return height; + }, [tags.isNotEmpty]); + return Scaffold( body: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ - HookConsumer( - builder: (context, ref, child) { - final tags = ref.watch(articleFilterProvider); - final feedTitle = ref.watch( - feedDataProvider( - feedId, - ).select((value) => value.value?.title.whenNotEmpty), - ); - - final focusNode = useFocusNode(); - final searchTextController = useTextEditingController(); - - final hasText = useListenableSelector( - searchTextController, - () => searchTextController.text.isNotEmpty, - ); - - useListenableCallback(searchTextController, () { - ref - .read(filteredArticleListProvider(feedId).notifier) - .search(searchTextController.text); - }); - - final bottomHeight = useMemoized(() { - var height = 56.0 + 4.0; - - if (tags.isNotEmpty) { - height += 48; - } - - return height; - }, [tags.isNotEmpty]); - - return SliverAppBar( - floating: true, - title: Text(feedTitle ?? 'Articles'), - bottom: PreferredSize( - preferredSize: Size(double.infinity, bottomHeight), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - TextField( - focusNode: focusNode, - controller: searchTextController, - decoration: InputDecoration( - label: const Text('Search'), - suffixIcon: hasText - ? IconButton( - onPressed: () { - searchTextController.clear(); - focusNode.requestFocus(); - }, - icon: const Icon(Icons.clear), - ) - : SpeechToTextButton( - onTextReceived: (data) { - searchTextController.text = data - .toString(); - }, - ), - ), - ), - const SizedBox(height: 4), - if (tags.isNotEmpty) - SizedBox( - width: double.infinity, - height: 48, - child: FadingScroll( - fadingSize: 15, - builder: (context, controller) { - return ListView( - controller: controller, - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: tags - .map( - (tag) => Padding( - padding: const EdgeInsets.only( - right: 8.0, - ), - child: FilterChip( - label: Text(tag), - showCheckmark: false, - selected: true, - onSelected: (value) {}, - onDeleted: () { - ref - .read( - articleFilterProvider - .notifier, - ) - .removeTag(tag); - }, - ), - ), - ) - .toList(), - ); - }, - ), - ), - ], + SliverAppBar( + floating: true, + title: Text(feedTitle ?? 'Articles'), + bottom: PreferredSize( + preferredSize: Size(double.infinity, bottomHeight), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + TextField( + focusNode: focusNode, + controller: searchTextController, + decoration: InputDecoration( + label: const Text('Search'), + suffixIcon: hasText + ? IconButton( + onPressed: () { + searchTextController.clear(); + focusNode.requestFocus(); + }, + icon: const Icon(Icons.clear), + ) + : SpeechToTextButton( + onTextReceived: (data) { + searchTextController.text = data.toString(); + }, + ), + ), ), - ), + const SizedBox(height: 4), + if (tags.isNotEmpty) + SizedBox( + width: double.infinity, + height: 48, + child: FadingScroll( + fadingSize: 15, + builder: (context, controller) { + return ListView( + controller: controller, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: tags + .map( + (tag) => Padding( + padding: const EdgeInsets.only( + right: 8.0, + ), + child: FilterChip( + label: Text(tag), + showCheckmark: false, + selected: true, + onSelected: (value) {}, + onDeleted: () { + ref + .read( + articleFilterProvider + .notifier, + ) + .removeTag(tag); + }, + ), + ), + ) + .toList(), + ); + }, + ), + ), + ], ), - ); - }, + ), + ), ), ]; }, - body: Consumer( - builder: (context, ref, child) { - final articlesAsync = ref.watch( - // ignore: provider_parameters - filteredArticleListProvider(feedId), - ); - - return articlesAsync.when( - skipLoadingOnReload: true, - data: (articles) { - return RefreshIndicator( - onRefresh: () async { - if (feedId != null) { - await ref - .read(fetchArticlesControllerProvider.notifier) - .fetchFeedArticles(feedId!); - } else { - await ref - .read(fetchArticlesControllerProvider.notifier) - .fetchAllArticles(); - } - }, - child: MediaQuery.removePadding( - removeTop: true, - context: context, - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: articles.length, - itemBuilder: (context, i) { - final article = articles[i]; - return FeedArticleCard( - key: ValueKey(article.id), - article: article, - ); - }, - ), - ), - ); + body: articlesAsync.when( + skipLoadingOnReload: true, + data: (articles) { + return RefreshIndicator( + onRefresh: () async { + if (feedId != null) { + await ref + .read(fetchArticlesControllerProvider.notifier) + .fetchFeedArticles(feedId!); + } else { + await ref + .read(fetchArticlesControllerProvider.notifier) + .fetchAllArticles(); + } }, - error: (error, stackTrace) => Center( - child: FailureWidget( - title: 'Failed to load Articles', - exception: error, + child: MediaQuery.removePadding( + removeTop: true, + context: context, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: articles.length, + itemBuilder: (context, i) { + final article = articles[i]; + return FeedArticleCard( + key: ValueKey(article.id), + article: article, + ); + }, ), ), - loading: () => const SizedBox.shrink(), ); }, + error: (error, stackTrace) => Center( + child: FailureWidget( + title: 'Failed to load Articles', + exception: error, + ), + ), + loading: () => const SizedBox.shrink(), ), ), );