avoid using builders

This commit is contained in:
Fabian Freund
2025-12-15 21:07:32 +01:00
parent e7a9d03bbc
commit b3a3b9930b
5 changed files with 575 additions and 595 deletions
@@ -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<bool> showAppBar;
final ValueNotifier<PersistentBottomSheetController?> 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,
@@ -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(<TabEntity>[]);
final itemCount =
filteredTabEntities.value.length +
//Limit to 3 sugegstions for now
math.min<int>(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<String?>(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(<TabEntity>[]);
final itemCount =
filteredTabEntities.value.length +
//Limit to 3 sugegstions for now
math.min<int>(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<String?>(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(
@@ -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 {
@@ -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<String?>(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<String?>(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 {
@@ -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(),
),
),
);