From c9234f79087bd4d06dff35fd4d40c558dd77f7e9 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Thu, 6 Jan 2022 07:29:30 -0500 Subject: [PATCH] Add jump button to lists, split up stores (again) --- lib/main.dart | 13 ++- lib/screens/channel/chat/chat_store.dart | 2 +- .../channel/chat/details/chat_users_list.dart | 10 +-- lib/screens/home/home.dart | 8 +- lib/screens/home/stores/categories_store.dart | 72 +++++++++++++++ .../home/stores/categories_store.g.dart | 90 +++++++++++++++++++ lib/screens/home/stores/list_store.dart | 67 ++++++-------- lib/screens/home/stores/list_store.g.dart | 53 ++++------- lib/screens/home/streams_list.dart | 58 +++++++++--- .../home/top/categories/categories.dart | 59 +++++++++--- .../home/top/categories/category_streams.dart | 88 ++++++++++-------- lib/screens/home/top/top_section.dart | 5 +- lib/widgets/scroll_to_top_button.dart | 22 +++++ 13 files changed, 393 insertions(+), 154 deletions(-) create mode 100644 lib/screens/home/stores/categories_store.dart create mode 100644 lib/screens/home/stores/categories_store.g.dart create mode 100644 lib/widgets/scroll_to_top_button.dart diff --git a/lib/main.dart b/lib/main.dart index eaac56f2..96247ec4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/screens/home/home.dart'; import 'package:frosty/screens/home/search/stores/search_store.dart'; +import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:mobx/mobx.dart'; @@ -24,8 +25,15 @@ Future main() async { final preferences = await SharedPreferences.getInstance(); final userSettings = preferences.getString('settings'); - // Initialize a settings store from existing settings. If existing settings don't exist create a new one. - final settingsStore = userSettings != null ? SettingsStore.fromJson(jsonDecode(userSettings)) : SettingsStore(); + SettingsStore settingsStore; + try { + // Initialize a settings store from existing settings. + settingsStore = SettingsStore.fromJson(jsonDecode(userSettings!)); + } catch (e) { + // If existing settings don't exist or if the settings could not be decoded properly, create a new one. + // This will also occur when new settings are added to the app. + settingsStore = SettingsStore(); + } // Create a MobX reaction that will save the settings on disk everytime they are changed. autorun((_) => preferences.setString('settings', jsonEncode(settingsStore))); @@ -96,6 +104,7 @@ class MyApp extends StatelessWidget { authStore: authStore, listType: ListType.top, ), + categoriesSectionStore: CategoriesStore(authStore: authStore), followedStreamsStore: authStore.isLoggedIn ? ListStore( authStore: authStore, diff --git a/lib/screens/channel/chat/chat_store.dart b/lib/screens/channel/chat/chat_store.dart index 196c413f..0146f4fa 100644 --- a/lib/screens/channel/chat/chat_store.dart +++ b/lib/screens/channel/chat/chat_store.dart @@ -63,7 +63,7 @@ abstract class _ChatStoreBase with Store { @readonly var _userState = const USERSTATE(); - late ReactionDisposer disposeEmoteMenuReaction; + late final ReactionDisposer disposeEmoteMenuReaction; _ChatStoreBase({ required this.auth, diff --git a/lib/screens/channel/chat/details/chat_users_list.dart b/lib/screens/channel/chat/details/chat_users_list.dart index 0c725092..d0e380b7 100644 --- a/lib/screens/channel/chat/details/chat_users_list.dart +++ b/lib/screens/channel/chat/details/chat_users_list.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:frosty/screens/channel/chat/details/chat_details_store.dart'; +import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:intl/intl.dart'; class ChattersList extends StatefulWidget { @@ -137,14 +138,7 @@ class _ChattersListState extends State { ) ], ), - if (_showJumpButton) - ElevatedButton.icon( - label: const Text('Scroll to Top'), - onPressed: () => _scrollController.jumpTo(0.0), - icon: const Icon( - Icons.arrow_circle_up, - ), - ), + if (_showJumpButton) ScrollToTopButton(scrollController: _scrollController) ], ), ), diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 8b43608e..b8796256 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -3,6 +3,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/screens/home/search/search.dart'; import 'package:frosty/screens/home/search/stores/search_store.dart'; +import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/streams_list.dart'; import 'package:frosty/screens/home/top/top_section.dart'; @@ -12,12 +13,14 @@ import 'package:provider/provider.dart'; class Home extends StatefulWidget { final ListStore topSectionStore; + final CategoriesStore categoriesSectionStore; final SearchStore searchStore; final ListStore? followedStreamsStore; const Home({ Key? key, required this.topSectionStore, + required this.categoriesSectionStore, required this.searchStore, required this.followedStreamsStore, }) : super(key: key); @@ -76,7 +79,10 @@ class _HomeState extends State { index: _selectedIndex, children: [ if (authStore.isLoggedIn) StreamsList(store: widget.followedStreamsStore!), - TopSection(topSectionStore: widget.topSectionStore), + TopSection( + topSectionStore: widget.topSectionStore, + categoriesSectionStore: widget.categoriesSectionStore, + ), Search(searchStore: widget.searchStore), ], ); diff --git a/lib/screens/home/stores/categories_store.dart b/lib/screens/home/stores/categories_store.dart new file mode 100644 index 00000000..80d8e4dd --- /dev/null +++ b/lib/screens/home/stores/categories_store.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:frosty/api/twitch_api.dart'; +import 'package:frosty/core/auth/auth_store.dart'; +import 'package:frosty/models/category.dart'; +import 'package:mobx/mobx.dart'; +part 'categories_store.g.dart'; + +class CategoriesStore = _CategoriesStoreBase with _$CategoriesStore; + +abstract class _CategoriesStoreBase with Store { + /// The authentication store. + final AuthStore authStore; + + final scrollController = ScrollController(); + + /// The pagination cursor for the categories. + String? _categoriesCursor; + + @observable + var showJumpButton = false; + + @readonly + var _categories = ObservableList(); + + /// The loading status for pagination. + @readonly + bool _isLoading = false; + + /// Returns whether or not there are more streams and loading status for pagination. + bool get hasMore => _isLoading == false && _categoriesCursor != null; + + _CategoriesStoreBase({required this.authStore}) { + scrollController.addListener(() { + if (scrollController.position.atEdge || scrollController.position.outOfRange) { + showJumpButton = false; + } else { + showJumpButton = true; + } + }); + getCategories(); + } + + // Fetches the top categories based on the current cursor. + @action + Future getCategories() async { + _isLoading = true; + + final result = await Twitch.getTopGames(headers: authStore.headersTwitch, cursor: _categoriesCursor); + if (result != null) { + if (_categoriesCursor == null) { + _categories = result.data.asObservable(); + } else { + _categories.addAll(result.data); + } + _categoriesCursor = result.pagination['cursor']; + } + + _isLoading = false; + } + + /// Resets the cursor and then fetches the categories. + @action + Future refreshCategories() { + _categoriesCursor = null; + + return getCategories(); + } + + void dispose() { + scrollController.dispose(); + } +} diff --git a/lib/screens/home/stores/categories_store.g.dart b/lib/screens/home/stores/categories_store.g.dart new file mode 100644 index 00000000..50b770ed --- /dev/null +++ b/lib/screens/home/stores/categories_store.g.dart @@ -0,0 +1,90 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'categories_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic + +mixin _$CategoriesStore on _CategoriesStoreBase, Store { + final _$showJumpButtonAtom = + Atom(name: '_CategoriesStoreBase.showJumpButton'); + + @override + bool get showJumpButton { + _$showJumpButtonAtom.reportRead(); + return super.showJumpButton; + } + + @override + set showJumpButton(bool value) { + _$showJumpButtonAtom.reportWrite(value, super.showJumpButton, () { + super.showJumpButton = value; + }); + } + + final _$_categoriesAtom = Atom(name: '_CategoriesStoreBase._categories'); + + ObservableList get categories { + _$_categoriesAtom.reportRead(); + return super._categories; + } + + @override + ObservableList get _categories => categories; + + @override + set _categories(ObservableList value) { + _$_categoriesAtom.reportWrite(value, super._categories, () { + super._categories = value; + }); + } + + final _$_isLoadingAtom = Atom(name: '_CategoriesStoreBase._isLoading'); + + bool get isLoading { + _$_isLoadingAtom.reportRead(); + return super._isLoading; + } + + @override + bool get _isLoading => isLoading; + + @override + set _isLoading(bool value) { + _$_isLoadingAtom.reportWrite(value, super._isLoading, () { + super._isLoading = value; + }); + } + + final _$getCategoriesAsyncAction = + AsyncAction('_CategoriesStoreBase.getCategories'); + + @override + Future getCategories() { + return _$getCategoriesAsyncAction.run(() => super.getCategories()); + } + + final _$_CategoriesStoreBaseActionController = + ActionController(name: '_CategoriesStoreBase'); + + @override + Future refreshCategories() { + final _$actionInfo = _$_CategoriesStoreBaseActionController.startAction( + name: '_CategoriesStoreBase.refreshCategories'); + try { + return super.refreshCategories(); + } finally { + _$_CategoriesStoreBaseActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +showJumpButton: ${showJumpButton} + '''; + } +} diff --git a/lib/screens/home/stores/list_store.dart b/lib/screens/home/stores/list_store.dart index 5015f328..b0db121a 100644 --- a/lib/screens/home/stores/list_store.dart +++ b/lib/screens/home/stores/list_store.dart @@ -1,3 +1,4 @@ +import 'package:flutter/widgets.dart'; import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/category.dart'; @@ -9,29 +10,28 @@ part 'list_store.g.dart'; class ListStore = _ListStoreBase with _$ListStore; abstract class _ListStoreBase with Store { - /// The list of the fetched streams. - @readonly - var _streams = ObservableList(); + /// The authentication store. + final AuthStore authStore; - @readonly - var _categories = ObservableList(); + final ListType listType; - /// The loading status for pagination. - @readonly - bool _isLoading = false; + final CategoryTwitch? categoryInfo; + + final scrollController = ScrollController(); /// The pagination cursor for the streams. String? _streamsCursor; - /// The pagination cursor for the categories. - String? _categoriesCursor; + @observable + var showJumpButton = false; - final CategoryTwitch? categoryInfo; + /// The list of the fetched streams. + @readonly + var _streams = ObservableList(); - final ListType listType; - - /// The authentication store. - final AuthStore authStore; + /// The loading status for pagination. + @readonly + bool _isLoading = false; /// Returns whether or not there are more streams and loading status for pagination. bool get hasMore => _isLoading == false && _streamsCursor != null; @@ -41,14 +41,19 @@ abstract class _ListStoreBase with Store { required this.listType, this.categoryInfo, }) { + scrollController.addListener(() { + if (scrollController.position.atEdge || scrollController.position.outOfRange) { + showJumpButton = false; + } else { + showJumpButton = true; + } + }); + switch (listType) { case ListType.followed: if (listType == ListType.followed && authStore.isLoggedIn) getStreams(); break; case ListType.top: - getStreams(); - getCategories(); - break; case ListType.category: getStreams(); break; @@ -96,24 +101,6 @@ abstract class _ListStoreBase with Store { _isLoading = false; } - // Fetches the top categories based on the current cursor. - @action - Future getCategories() async { - _isLoading = true; - - final result = await Twitch.getTopGames(headers: authStore.headersTwitch, cursor: _categoriesCursor); - if (result != null) { - if (_categoriesCursor == null) { - _categories = result.data.asObservable(); - } else { - _categories.addAll(result.data); - } - _categoriesCursor = result.pagination['cursor']; - } - - _isLoading = false; - } - /// Resets the cursor and then fetches the streams. @action Future refreshStreams() { @@ -122,12 +109,8 @@ abstract class _ListStoreBase with Store { return getStreams(); } - /// Resets the cursor and then fetches the categories. - @action - Future refreshCategories() { - _categoriesCursor = null; - - return getCategories(); + void dispose() { + scrollController.dispose(); } } diff --git a/lib/screens/home/stores/list_store.g.dart b/lib/screens/home/stores/list_store.g.dart index 17b926ec..33c33db2 100644 --- a/lib/screens/home/stores/list_store.g.dart +++ b/lib/screens/home/stores/list_store.g.dart @@ -9,6 +9,21 @@ part of 'list_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic mixin _$ListStore on _ListStoreBase, Store { + final _$showJumpButtonAtom = Atom(name: '_ListStoreBase.showJumpButton'); + + @override + bool get showJumpButton { + _$showJumpButtonAtom.reportRead(); + return super.showJumpButton; + } + + @override + set showJumpButton(bool value) { + _$showJumpButtonAtom.reportWrite(value, super.showJumpButton, () { + super.showJumpButton = value; + }); + } + final _$_streamsAtom = Atom(name: '_ListStoreBase._streams'); ObservableList get streams { @@ -26,23 +41,6 @@ mixin _$ListStore on _ListStoreBase, Store { }); } - final _$_categoriesAtom = Atom(name: '_ListStoreBase._categories'); - - ObservableList get categories { - _$_categoriesAtom.reportRead(); - return super._categories; - } - - @override - ObservableList get _categories => categories; - - @override - set _categories(ObservableList value) { - _$_categoriesAtom.reportWrite(value, super._categories, () { - super._categories = value; - }); - } - final _$_isLoadingAtom = Atom(name: '_ListStoreBase._isLoading'); bool get isLoading { @@ -67,14 +65,6 @@ mixin _$ListStore on _ListStoreBase, Store { return _$getStreamsAsyncAction.run(() => super.getStreams()); } - final _$getCategoriesAsyncAction = - AsyncAction('_ListStoreBase.getCategories'); - - @override - Future getCategories() { - return _$getCategoriesAsyncAction.run(() => super.getCategories()); - } - final _$_ListStoreBaseActionController = ActionController(name: '_ListStoreBase'); @@ -89,21 +79,10 @@ mixin _$ListStore on _ListStoreBase, Store { } } - @override - Future refreshCategories() { - final _$actionInfo = _$_ListStoreBaseActionController.startAction( - name: '_ListStoreBase.refreshCategories'); - try { - return super.refreshCategories(); - } finally { - _$_ListStoreBaseActionController.endAction(_$actionInfo); - } - } - @override String toString() { return ''' - +showJumpButton: ${showJumpButton} '''; } } diff --git a/lib/screens/home/streams_list.dart b/lib/screens/home/streams_list.dart index 009271da..29c2706b 100644 --- a/lib/screens/home/streams_list.dart +++ b/lib/screens/home/streams_list.dart @@ -3,16 +3,26 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/widgets/loading_indicator.dart'; +import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:frosty/widgets/stream_card.dart'; import 'package:provider/provider.dart'; -class StreamsList extends StatelessWidget { +class StreamsList extends StatefulWidget { final ListStore store; const StreamsList({Key? key, required this.store}) : super(key: key); + @override + _StreamsListState createState() => _StreamsListState(); +} + +class _StreamsListState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { + super.build(context); + final store = widget.store; + store.refreshStreams(); + return RefreshIndicator( onRefresh: store.refreshStreams, child: Observer( @@ -20,22 +30,44 @@ class StreamsList extends StatelessWidget { if (store.streams.isEmpty && store.isLoading) { return const LoadingIndicator(subtitle: Text('Loading streams...')); } - return ListView.builder( - itemCount: store.streams.length, - itemBuilder: (context, index) { - if (index > store.streams.length / 2 && store.hasMore) { - store.getStreams(); - } - return Observer( - builder: (context) => StreamCard( - streamInfo: store.streams[index], - showUptime: context.read().showThumbnailUptime, + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + controller: store.scrollController, + itemCount: store.streams.length, + itemBuilder: (context, index) { + if (index > store.streams.length / 2 && store.hasMore) { + store.getStreams(); + } + return Observer( + builder: (context) => StreamCard( + streamInfo: store.streams[index], + showUptime: context.read().showThumbnailUptime, + ), + ); + }, + ), + Observer( + builder: (context) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), ), - ); - }, + ), + ], ); }, ), ); } + + @override + bool get wantKeepAlive => true; + + @override + void dispose() { + widget.store.dispose(); + super.dispose(); + } } diff --git a/lib/screens/home/top/categories/categories.dart b/lib/screens/home/top/categories/categories.dart index 055ad349..96015792 100644 --- a/lib/screens/home/top/categories/categories.dart +++ b/lib/screens/home/top/categories/categories.dart @@ -1,36 +1,67 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:frosty/screens/home/stores/list_store.dart'; +import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/top/categories/category_card.dart'; +import 'package:frosty/widgets/scroll_to_top_button.dart'; -class Categories extends StatelessWidget { - final ListStore store; +class Categories extends StatefulWidget { + final CategoriesStore store; const Categories({ Key? key, required this.store, }) : super(key: key); + @override + _CategoriesState createState() => _CategoriesState(); +} + +class _CategoriesState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { + super.build(context); + final store = widget.store; + store.refreshCategories(); + return RefreshIndicator( onRefresh: store.refreshCategories, child: Observer( builder: (_) { - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - ), - itemCount: store.categories.length, - itemBuilder: (context, index) { - if (index > store.categories.length / 2 && store.hasMore) { - store.getCategories(); - } - return CategoryCard(category: store.categories[index]); - }, + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + GridView.builder( + controller: store.scrollController, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: store.categories.length, + itemBuilder: (context, index) { + if (index > store.categories.length / 2 && store.hasMore) { + store.getCategories(); + } + return CategoryCard(category: store.categories[index]); + }, + ), + Observer( + builder: (context) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), + ), + ), + ], ); }, ), ); } + + @override + bool get wantKeepAlive => true; + + @override + void dispose() { + widget.store.dispose(); + super.dispose(); + } } diff --git a/lib/screens/home/top/categories/category_streams.dart b/lib/screens/home/top/categories/category_streams.dart index 841fbc79..4f43cb8e 100644 --- a/lib/screens/home/top/categories/category_streams.dart +++ b/lib/screens/home/top/categories/category_streams.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; +import 'package:frosty/widgets/scroll_to_top_button.dart'; import 'package:frosty/widgets/stream_card.dart'; import 'package:provider/provider.dart'; @@ -18,42 +19,59 @@ class CategoryStreams extends StatelessWidget { onRefresh: store.refreshStreams, child: Observer( builder: (context) { - return CustomScrollView( - physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - slivers: [ - SliverAppBar( - stretch: true, - pinned: true, - expandedHeight: MediaQuery.of(context).size.height / 3, - flexibleSpace: FlexibleSpaceBar( - title: Text( - store.categoryInfo!.name, - style: Theme.of(context).textTheme.headline6?.copyWith(fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - background: CachedNetworkImage( - imageUrl: store.categoryInfo!.boxArtUrl.replaceRange(store.categoryInfo!.boxArtUrl.lastIndexOf('-') + 1, null, '300x400.jpg'), - color: const Color.fromRGBO(255, 255, 255, 0.5), - colorBlendMode: BlendMode.modulate, - fit: BoxFit.cover, - ), - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index > store.streams.length / 2 && store.hasMore) { - store.getStreams(); - } - return Observer( - builder: (context) => StreamCard( - streamInfo: store.streams[index], - showUptime: context.read().showThumbnailUptime, + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + CustomScrollView( + physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + controller: store.scrollController, + slivers: [ + SliverAppBar( + stretch: true, + pinned: true, + expandedHeight: MediaQuery.of(context).size.height / 3, + flexibleSpace: FlexibleSpaceBar( + title: Text( + store.categoryInfo!.name, + style: Theme.of(context).textTheme.headline6?.copyWith(fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ); - }, - childCount: store.streams.length, + background: CachedNetworkImage( + imageUrl: store.categoryInfo!.boxArtUrl.replaceRange(store.categoryInfo!.boxArtUrl.lastIndexOf('-') + 1, null, '300x400.jpg'), + color: const Color.fromRGBO(255, 255, 255, 0.5), + colorBlendMode: BlendMode.modulate, + fit: BoxFit.cover, + ), + ), + ), + SliverSafeArea( + top: false, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index > store.streams.length / 2 && store.hasMore) { + store.getStreams(); + } + return Observer( + builder: (context) => StreamCard( + streamInfo: store.streams[index], + showUptime: context.read().showThumbnailUptime, + ), + ); + }, + childCount: store.streams.length, + ), + ), + ), + ], + ), + SafeArea( + child: Observer( + builder: (context) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), + ), ), ), ], diff --git a/lib/screens/home/top/top_section.dart b/lib/screens/home/top/top_section.dart index 804b086e..9dcc0ecd 100644 --- a/lib/screens/home/top/top_section.dart +++ b/lib/screens/home/top/top_section.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/streams_list.dart'; import 'package:frosty/screens/home/top/categories/categories.dart'; class TopSection extends StatelessWidget { final ListStore topSectionStore; + final CategoriesStore categoriesSectionStore; const TopSection({ Key? key, required this.topSectionStore, + required this.categoriesSectionStore, }) : super(key: key); @override @@ -27,7 +30,7 @@ class TopSection extends StatelessWidget { child: TabBarView( children: [ StreamsList(store: topSectionStore), - Categories(store: topSectionStore), + Categories(store: categoriesSectionStore), ], ), ), diff --git a/lib/widgets/scroll_to_top_button.dart b/lib/widgets/scroll_to_top_button.dart new file mode 100644 index 00000000..370d265f --- /dev/null +++ b/lib/widgets/scroll_to_top_button.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class ScrollToTopButton extends StatelessWidget { + final ScrollController scrollController; + + const ScrollToTopButton({Key? key, required this.scrollController}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + label: const Text('Scroll to Top'), + onPressed: () => scrollController.animateTo( + 0.0, + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 500), + ), + icon: const Icon( + Icons.arrow_circle_up, + ), + ); + } +}