Add jump button to lists, split up stores (again)
This commit is contained in:
parent
f3688f3696
commit
c9234f7908
13 changed files with 393 additions and 154 deletions
|
|
@ -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<void> 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ChattersList> {
|
|||
)
|
||||
],
|
||||
),
|
||||
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)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<Home> {
|
|||
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),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
72
lib/screens/home/stores/categories_store.dart
Normal file
72
lib/screens/home/stores/categories_store.dart
Normal file
|
|
@ -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<CategoryTwitch>();
|
||||
|
||||
/// 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<void> 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<void> refreshCategories() {
|
||||
_categoriesCursor = null;
|
||||
|
||||
return getCategories();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
}
|
||||
}
|
||||
90
lib/screens/home/stores/categories_store.g.dart
Normal file
90
lib/screens/home/stores/categories_store.g.dart
Normal file
|
|
@ -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<CategoryTwitch> get categories {
|
||||
_$_categoriesAtom.reportRead();
|
||||
return super._categories;
|
||||
}
|
||||
|
||||
@override
|
||||
ObservableList<CategoryTwitch> get _categories => categories;
|
||||
|
||||
@override
|
||||
set _categories(ObservableList<CategoryTwitch> 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<void> getCategories() {
|
||||
return _$getCategoriesAsyncAction.run(() => super.getCategories());
|
||||
}
|
||||
|
||||
final _$_CategoriesStoreBaseActionController =
|
||||
ActionController(name: '_CategoriesStoreBase');
|
||||
|
||||
@override
|
||||
Future<void> refreshCategories() {
|
||||
final _$actionInfo = _$_CategoriesStoreBaseActionController.startAction(
|
||||
name: '_CategoriesStoreBase.refreshCategories');
|
||||
try {
|
||||
return super.refreshCategories();
|
||||
} finally {
|
||||
_$_CategoriesStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
showJumpButton: ${showJumpButton}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StreamTwitch>();
|
||||
/// The authentication store.
|
||||
final AuthStore authStore;
|
||||
|
||||
@readonly
|
||||
var _categories = ObservableList<CategoryTwitch>();
|
||||
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<StreamTwitch>();
|
||||
|
||||
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<void> 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<void> refreshStreams() {
|
||||
|
|
@ -122,12 +109,8 @@ abstract class _ListStoreBase with Store {
|
|||
return getStreams();
|
||||
}
|
||||
|
||||
/// Resets the cursor and then fetches the categories.
|
||||
@action
|
||||
Future<void> refreshCategories() {
|
||||
_categoriesCursor = null;
|
||||
|
||||
return getCategories();
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<StreamTwitch> get streams {
|
||||
|
|
@ -26,23 +41,6 @@ mixin _$ListStore on _ListStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$_categoriesAtom = Atom(name: '_ListStoreBase._categories');
|
||||
|
||||
ObservableList<CategoryTwitch> get categories {
|
||||
_$_categoriesAtom.reportRead();
|
||||
return super._categories;
|
||||
}
|
||||
|
||||
@override
|
||||
ObservableList<CategoryTwitch> get _categories => categories;
|
||||
|
||||
@override
|
||||
set _categories(ObservableList<CategoryTwitch> 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<void> getCategories() {
|
||||
return _$getCategoriesAsyncAction.run(() => super.getCategories());
|
||||
}
|
||||
|
||||
final _$_ListStoreBaseActionController =
|
||||
ActionController(name: '_ListStoreBase');
|
||||
|
||||
|
|
@ -89,21 +79,10 @@ mixin _$ListStore on _ListStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refreshCategories() {
|
||||
final _$actionInfo = _$_ListStoreBaseActionController.startAction(
|
||||
name: '_ListStoreBase.refreshCategories');
|
||||
try {
|
||||
return super.refreshCategories();
|
||||
} finally {
|
||||
_$_ListStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
||||
showJumpButton: ${showJumpButton}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<StreamsList> 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<SettingsStore>().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<SettingsStore>().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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Categories> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SettingsStore>().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<SettingsStore>().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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
22
lib/widgets/scroll_to_top_button.dart
Normal file
22
lib/widgets/scroll_to_top_button.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue