Refactor & split stream list
This commit is contained in:
parent
a6654d9b89
commit
d7633281d2
18 changed files with 341 additions and 280 deletions
|
|
@ -159,7 +159,7 @@ class Twitch {
|
|||
final decoded = jsonDecode(response.body);
|
||||
final data = decoded['data'] as List;
|
||||
|
||||
return {'streams': data.map((stream) => Stream.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']};
|
||||
return {'streams': data.map((stream) => StreamTwitch.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']};
|
||||
} else {
|
||||
debugPrint('Failed to update top streams');
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ class Twitch {
|
|||
final decoded = jsonDecode(response.body);
|
||||
final data = decoded['data'] as List;
|
||||
|
||||
return {'streams': data.map((stream) => Stream.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']};
|
||||
return {'streams': data.map((stream) => StreamTwitch.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']};
|
||||
} else {
|
||||
debugPrint('Failed to update followed streams');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/core/settings/settings_store.dart';
|
||||
import 'package:frosty/screens/categories/categories_store.dart';
|
||||
import 'package:frosty/screens/home/home.dart';
|
||||
import 'package:frosty/screens/home/home_store.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_list_store.dart';
|
||||
import 'package:frosty/screens/search/search_store.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_followed/followed_streams_store.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_top/top_streams_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -58,9 +61,14 @@ class MyApp extends StatelessWidget {
|
|||
]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
final authStore = context.read<AuthStore>();
|
||||
|
||||
return Home(
|
||||
streamListStore: StreamListStore(authStore: context.read<AuthStore>()),
|
||||
homeStore: HomeStore(),
|
||||
topStreamsStore: TopStreamsStore(authStore: authStore),
|
||||
followedStreamsStore: FollowedStreamsStore(authStore: authStore),
|
||||
categoriesStore: CategoriesStore(authStore: authStore),
|
||||
searchStore: SearchStore(authStore: authStore),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ part 'stream.g.dart';
|
|||
|
||||
// Object for live Twitch streams.
|
||||
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
|
||||
class Stream {
|
||||
class StreamTwitch {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String userLogin;
|
||||
|
|
@ -20,7 +20,7 @@ class Stream {
|
|||
final List<String>? tagIds;
|
||||
final bool isMature;
|
||||
|
||||
const Stream(
|
||||
const StreamTwitch(
|
||||
this.id,
|
||||
this.userId,
|
||||
this.userLogin,
|
||||
|
|
@ -37,5 +37,5 @@ class Stream {
|
|||
this.isMature,
|
||||
);
|
||||
|
||||
factory Stream.fromJson(Map<String, dynamic> json) => _$StreamFromJson(json);
|
||||
factory StreamTwitch.fromJson(Map<String, dynamic> json) => _$StreamTwitchFromJson(json);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'stream.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Stream _$StreamFromJson(Map<String, dynamic> json) => Stream(
|
||||
StreamTwitch _$StreamTwitchFromJson(Map<String, dynamic> json) => StreamTwitch(
|
||||
json['id'] as String,
|
||||
json['user_id'] as String,
|
||||
json['user_login'] as String,
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/categories/categories_store.dart';
|
||||
import 'package:frosty/screens/categories/category_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class Categories extends StatelessWidget {
|
||||
const Categories({Key? key}) : super(key: key);
|
||||
final CategoriesStore store;
|
||||
|
||||
const Categories({
|
||||
Key? key,
|
||||
required this.store,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesStore = CategoriesStore(authStore: context.read<AuthStore>());
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
return ListView.builder(
|
||||
itemCount: categoriesStore.categories.length,
|
||||
itemCount: store.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > categoriesStore.categories.length / 2 && categoriesStore.hasMore) {
|
||||
categoriesStore.getGames();
|
||||
if (index > store.categories.length / 2 && store.hasMore) {
|
||||
store.getGames();
|
||||
}
|
||||
return CategoryCard(category: categoriesStore.categories[index]);
|
||||
return CategoryCard(category: store.categories[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,30 +4,32 @@ import 'package:frosty/core/auth/auth_store.dart';
|
|||
import 'package:frosty/core/settings/settings.dart';
|
||||
import 'package:frosty/core/settings/settings_store.dart';
|
||||
import 'package:frosty/screens/categories/categories.dart';
|
||||
import 'package:frosty/screens/categories/categories_store.dart';
|
||||
import 'package:frosty/screens/home/home_store.dart';
|
||||
import 'package:frosty/screens/search/search.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_list_store.dart';
|
||||
import 'package:frosty/screens/search/search_store.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_followed/followed_streams.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_followed/followed_streams_store.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_top/top_streams.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_top/top_streams_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../stream_list/stream_list.dart';
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
class Home extends StatelessWidget {
|
||||
final HomeStore homeStore;
|
||||
final StreamListStore streamListStore;
|
||||
final TopStreamsStore topStreamsStore;
|
||||
final FollowedStreamsStore followedStreamsStore;
|
||||
final CategoriesStore categoriesStore;
|
||||
final SearchStore searchStore;
|
||||
|
||||
const Home({
|
||||
Key? key,
|
||||
required this.homeStore,
|
||||
required this.streamListStore,
|
||||
required this.topStreamsStore,
|
||||
required this.followedStreamsStore,
|
||||
required this.categoriesStore,
|
||||
required this.searchStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomeState createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
final _textController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('build home');
|
||||
|
|
@ -43,7 +45,7 @@ class _HomeState extends State<Home> {
|
|||
'Search',
|
||||
];
|
||||
return Text(
|
||||
titles[widget.homeStore.selectedIndex],
|
||||
titles[homeStore.selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -66,19 +68,12 @@ class _HomeState extends State<Home> {
|
|||
body: Observer(
|
||||
builder: (_) {
|
||||
return IndexedStack(
|
||||
index: widget.homeStore.selectedIndex,
|
||||
index: homeStore.selectedIndex,
|
||||
children: [
|
||||
if (context.read<AuthStore>().isLoggedIn)
|
||||
StreamList(
|
||||
category: StreamCategory.followed,
|
||||
streamListStore: widget.streamListStore,
|
||||
),
|
||||
StreamList(
|
||||
category: StreamCategory.top,
|
||||
streamListStore: widget.streamListStore,
|
||||
),
|
||||
const Categories(),
|
||||
const Search(),
|
||||
if (context.read<AuthStore>().isLoggedIn) FollowedStreams(store: followedStreamsStore),
|
||||
TopStreams(store: topStreamsStore),
|
||||
Categories(store: categoriesStore),
|
||||
Search(store: searchStore),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
@ -106,17 +101,11 @@ class _HomeState extends State<Home> {
|
|||
label: 'Search',
|
||||
),
|
||||
],
|
||||
currentIndex: widget.homeStore.selectedIndex,
|
||||
onTap: widget.homeStore.handleTap,
|
||||
currentIndex: homeStore.selectedIndex,
|
||||
onTap: homeStore.handleTap,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/channel/video_chat.dart';
|
||||
import 'package:frosty/screens/search/search_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
const Search({Key? key}) : super(key: key);
|
||||
final SearchStore store;
|
||||
|
||||
const Search({
|
||||
Key? key,
|
||||
required this.store,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SearchState createState() => _SearchState();
|
||||
}
|
||||
|
||||
class _SearchState extends State<Search> {
|
||||
late final SearchStore searchStore;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
searchStore = SearchStore(authStore: context.read<AuthStore>());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
|
@ -28,7 +23,7 @@ class _SearchState extends State<Search> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
child: TextField(
|
||||
controller: searchStore.textController,
|
||||
controller: widget.store.textController,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
|
|
@ -38,28 +33,28 @@ class _SearchState extends State<Search> {
|
|||
),
|
||||
hintText: 'Search for a channel',
|
||||
),
|
||||
onSubmitted: searchStore.handleQuery,
|
||||
onSubmitted: widget.store.handleQuery,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => searchStore.handleQuery(searchStore.textController.text),
|
||||
onRefresh: () => widget.store.handleQuery(widget.store.textController.text),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return ListView.builder(
|
||||
itemCount: searchStore.searchResults.length + 1,
|
||||
itemCount: widget.store.searchResults.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == searchStore.searchResults.length) {
|
||||
if (searchStore.textController.text.isEmpty) {
|
||||
if (index == widget.store.searchResults.length) {
|
||||
if (widget.store.textController.text.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return ListTile(
|
||||
title: Text('Go to ${searchStore.textController.text}'),
|
||||
onTap: () => searchStore.handleSearch(searchStore.textController.text, context),
|
||||
title: Text('Go to ${widget.store.textController.text}'),
|
||||
onTap: () => widget.store.handleSearch(widget.store.textController.text, context),
|
||||
);
|
||||
}
|
||||
|
||||
final channel = searchStore.searchResults[index];
|
||||
final channel = widget.store.searchResults[index];
|
||||
return ListTile(
|
||||
title: Text(channel.displayName),
|
||||
trailing: channel.isLive
|
||||
|
|
@ -104,7 +99,7 @@ class _SearchState extends State<Search> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
searchStore.dispose();
|
||||
widget.store.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:intl/intl.dart';
|
|||
|
||||
/// A tappable card widget that displays a stream's thumbnail and details.
|
||||
class StreamCard extends StatelessWidget {
|
||||
final Stream streamInfo;
|
||||
final StreamTwitch streamInfo;
|
||||
|
||||
const StreamCard({Key? key, required this.streamInfo}) : super(key: key);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_card.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_list_store.dart';
|
||||
|
||||
class StreamList extends StatelessWidget {
|
||||
final StreamCategory category;
|
||||
final StreamListStore streamListStore;
|
||||
|
||||
const StreamList({Key? key, required this.category, required this.streamListStore}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => streamListStore.refresh(category: category),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return ListView.builder(
|
||||
itemCount: streamListStore.streams(category: category).length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > streamListStore.streams(category: category).length / 2 && streamListStore.hasMore(category: category)) {
|
||||
streamListStore.getStreams(category: category);
|
||||
}
|
||||
return StreamCard(streamInfo: streamListStore.streams(category: category)[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'stream_list_store.g.dart';
|
||||
|
||||
// TODO: Fix followed list refresh when log in.
|
||||
class StreamListStore = _StreamListBase with _$StreamListStore;
|
||||
|
||||
abstract class _StreamListBase with Store {
|
||||
/// The list of the fetched top streams.
|
||||
@observable
|
||||
var _topStreams = ObservableList<Stream>();
|
||||
|
||||
/// The list of the fetched followed streams.
|
||||
@observable
|
||||
var _followedStreams = ObservableList<Stream>();
|
||||
|
||||
/// The loading status for pagination.
|
||||
bool _isLoading = false;
|
||||
|
||||
/// The pagination cursor for top streams.
|
||||
String? _topStreamsCurrentCursor;
|
||||
|
||||
/// The pagination cursor for followed streams.
|
||||
String? _followedStreamsCurrentCursor;
|
||||
|
||||
/// The authentication store.
|
||||
final AuthStore authStore;
|
||||
|
||||
_StreamListBase({required this.authStore}) {
|
||||
getStreams(category: StreamCategory.top);
|
||||
|
||||
if (authStore.user?.id != null) {
|
||||
getStreams(category: StreamCategory.followed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the appropriate streams given the [category].
|
||||
ObservableList<Stream> streams({required StreamCategory category}) {
|
||||
switch (category) {
|
||||
case StreamCategory.top:
|
||||
return _topStreams;
|
||||
case StreamCategory.followed:
|
||||
return _followedStreams;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not there are more streams for the [category].
|
||||
bool hasMore({required StreamCategory category}) {
|
||||
switch (category) {
|
||||
case StreamCategory.top:
|
||||
return _isLoading == false && _topStreamsCurrentCursor != null;
|
||||
case StreamCategory.followed:
|
||||
return _isLoading == false && _followedStreamsCurrentCursor != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the cursor and then fetches the streams for the [category].
|
||||
@action
|
||||
Future<void> refresh({required StreamCategory category}) async {
|
||||
switch (category) {
|
||||
case StreamCategory.top:
|
||||
_topStreamsCurrentCursor = null;
|
||||
break;
|
||||
case StreamCategory.followed:
|
||||
_followedStreamsCurrentCursor = null;
|
||||
break;
|
||||
}
|
||||
|
||||
await getStreams(category: category);
|
||||
}
|
||||
|
||||
/// Fetches the streams for the [category] based on the current cursor.
|
||||
@action
|
||||
Future<void> getStreams({required StreamCategory category}) async {
|
||||
_isLoading = true;
|
||||
|
||||
switch (category) {
|
||||
case StreamCategory.top:
|
||||
final newTopStreams = await Twitch.getTopStreams(headers: authStore.headersTwitch, cursor: _topStreamsCurrentCursor);
|
||||
|
||||
if (newTopStreams != null) {
|
||||
if (_topStreamsCurrentCursor == null) {
|
||||
_topStreams = ObservableList.of(newTopStreams['streams']);
|
||||
} else {
|
||||
_topStreams.addAll(newTopStreams['streams']);
|
||||
}
|
||||
_topStreamsCurrentCursor = newTopStreams['cursor'];
|
||||
}
|
||||
break;
|
||||
case StreamCategory.followed:
|
||||
final newFollowedStreams =
|
||||
await Twitch.getFollowedStreams(id: authStore.user!.id, headers: authStore.headersTwitch, cursor: _followedStreamsCurrentCursor);
|
||||
|
||||
if (newFollowedStreams != null) {
|
||||
if (_followedStreamsCurrentCursor == null) {
|
||||
_followedStreams = ObservableList.of(newFollowedStreams['streams']);
|
||||
} else {
|
||||
_followedStreams.addAll(newFollowedStreams['streams']);
|
||||
}
|
||||
_followedStreamsCurrentCursor = newFollowedStreams['cursor'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of category for streams.
|
||||
enum StreamCategory { top, followed }
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'stream_list_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 _$StreamListStore on _StreamListBase, Store {
|
||||
final _$_topStreamsAtom = Atom(name: '_StreamListBase._topStreams');
|
||||
|
||||
@override
|
||||
ObservableList<Stream> get _topStreams {
|
||||
_$_topStreamsAtom.reportRead();
|
||||
return super._topStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
set _topStreams(ObservableList<Stream> value) {
|
||||
_$_topStreamsAtom.reportWrite(value, super._topStreams, () {
|
||||
super._topStreams = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$_followedStreamsAtom = Atom(name: '_StreamListBase._followedStreams');
|
||||
|
||||
@override
|
||||
ObservableList<Stream> get _followedStreams {
|
||||
_$_followedStreamsAtom.reportRead();
|
||||
return super._followedStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
set _followedStreams(ObservableList<Stream> value) {
|
||||
_$_followedStreamsAtom.reportWrite(value, super._followedStreams, () {
|
||||
super._followedStreams = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$refreshAsyncAction = AsyncAction('_StreamListBase.refresh');
|
||||
|
||||
@override
|
||||
Future<void> refresh({required StreamCategory category}) {
|
||||
return _$refreshAsyncAction.run(() => super.refresh(category: category));
|
||||
}
|
||||
|
||||
final _$getStreamsAsyncAction = AsyncAction('_StreamListBase.getStreams');
|
||||
|
||||
@override
|
||||
Future<void> getStreams({required StreamCategory category}) {
|
||||
return _$getStreamsAsyncAction
|
||||
.run(() => super.getStreams(category: category));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_card.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_followed/followed_streams_store.dart';
|
||||
|
||||
class FollowedStreams extends StatelessWidget {
|
||||
final FollowedStreamsStore store;
|
||||
|
||||
const FollowedStreams({Key? key, required this.store}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: store.refresh,
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return ListView.builder(
|
||||
itemCount: store.followedStreams.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > store.followedStreams.length / 2 && store.hasMore) {
|
||||
store.getFollowedStreams();
|
||||
}
|
||||
return StreamCard(streamInfo: store.followedStreams[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'followed_streams_store.g.dart';
|
||||
|
||||
class FollowedStreamsStore = _FollowedStreamsStoreBase with _$FollowedStreamsStore;
|
||||
|
||||
abstract class _FollowedStreamsStoreBase with Store {
|
||||
/// The list of the fetched followed streams.
|
||||
@observable
|
||||
var _followedStreams = ObservableList<StreamTwitch>();
|
||||
ObservableList<StreamTwitch> get followedStreams => _followedStreams;
|
||||
|
||||
/// The loading status for pagination.
|
||||
bool _isLoading = false;
|
||||
|
||||
/// The pagination cursor for followed streams.
|
||||
String? _followedStreamsCurrentCursor;
|
||||
|
||||
/// The authentication store.
|
||||
final AuthStore authStore;
|
||||
|
||||
/// Returns whether or not there are more streams and loading status for pagination.
|
||||
bool get hasMore => _isLoading == false && _followedStreamsCurrentCursor != null;
|
||||
|
||||
_FollowedStreamsStoreBase({required this.authStore}) {
|
||||
getFollowedStreams();
|
||||
}
|
||||
|
||||
/// Fetches the followed streams based on the current cursor.
|
||||
@action
|
||||
Future<void> getFollowedStreams() async {
|
||||
_isLoading = true;
|
||||
|
||||
final newFollowedStreams = await Twitch.getFollowedStreams(id: authStore.user!.id, headers: authStore.headersTwitch, cursor: _followedStreamsCurrentCursor);
|
||||
|
||||
if (newFollowedStreams != null) {
|
||||
if (_followedStreamsCurrentCursor == null) {
|
||||
_followedStreams = ObservableList.of(newFollowedStreams['streams']);
|
||||
} else {
|
||||
_followedStreams.addAll(newFollowedStreams['streams']);
|
||||
}
|
||||
_followedStreamsCurrentCursor = newFollowedStreams['cursor'];
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
/// Resets the cursor and then fetches the followed streams.
|
||||
@action
|
||||
Future<void> refresh() async {
|
||||
_followedStreamsCurrentCursor = null;
|
||||
|
||||
await getFollowedStreams();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'followed_streams_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 _$FollowedStreamsStore on _FollowedStreamsStoreBase, Store {
|
||||
final _$_followedStreamsAtom =
|
||||
Atom(name: '_FollowedStreamsStoreBase._followedStreams');
|
||||
|
||||
@override
|
||||
ObservableList<StreamTwitch> get _followedStreams {
|
||||
_$_followedStreamsAtom.reportRead();
|
||||
return super._followedStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
set _followedStreams(ObservableList<StreamTwitch> value) {
|
||||
_$_followedStreamsAtom.reportWrite(value, super._followedStreams, () {
|
||||
super._followedStreams = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$getFollowedStreamsAsyncAction =
|
||||
AsyncAction('_FollowedStreamsStoreBase.getFollowedStreams');
|
||||
|
||||
@override
|
||||
Future<void> getFollowedStreams() {
|
||||
return _$getFollowedStreamsAsyncAction
|
||||
.run(() => super.getFollowedStreams());
|
||||
}
|
||||
|
||||
final _$refreshAsyncAction = AsyncAction('_FollowedStreamsStoreBase.refresh');
|
||||
|
||||
@override
|
||||
Future<void> refresh() {
|
||||
return _$refreshAsyncAction.run(() => super.refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
30
lib/screens/stream_list/streams_top/top_streams.dart
Normal file
30
lib/screens/stream_list/streams_top/top_streams.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/stream_list/stream_card.dart';
|
||||
import 'package:frosty/screens/stream_list/streams_top/top_streams_store.dart';
|
||||
|
||||
class TopStreams extends StatelessWidget {
|
||||
final TopStreamsStore store;
|
||||
|
||||
const TopStreams({Key? key, required this.store}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: store.refresh,
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return ListView.builder(
|
||||
itemCount: store.topStreams.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > store.topStreams.length / 2 && store.hasMore) {
|
||||
store.getTopStreams();
|
||||
}
|
||||
return StreamCard(streamInfo: store.topStreams[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/screens/stream_list/streams_top/top_streams_store.dart
Normal file
58
lib/screens/stream_list/streams_top/top_streams_store.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'top_streams_store.g.dart';
|
||||
|
||||
class TopStreamsStore = _TopStreamsStoreBase with _$TopStreamsStore;
|
||||
|
||||
abstract class _TopStreamsStoreBase with Store {
|
||||
/// The list of the fetched top streams.
|
||||
@observable
|
||||
var _topStreams = ObservableList<StreamTwitch>();
|
||||
ObservableList<StreamTwitch> get topStreams => _topStreams;
|
||||
|
||||
/// The loading status for pagination.
|
||||
bool _isLoading = false;
|
||||
|
||||
/// The pagination cursor for top streams.
|
||||
String? _topStreamsCurrentCursor;
|
||||
|
||||
/// The authentication store.
|
||||
final AuthStore authStore;
|
||||
|
||||
/// Returns whether or not there are more streams and loading status for pagination.
|
||||
bool get hasMore => _isLoading == false && _topStreamsCurrentCursor != null;
|
||||
|
||||
_TopStreamsStoreBase({required this.authStore}) {
|
||||
getTopStreams();
|
||||
}
|
||||
|
||||
/// Fetches the top streams based on the current cursor.
|
||||
@action
|
||||
Future<void> getTopStreams() async {
|
||||
_isLoading = true;
|
||||
|
||||
final newTopStreams = await Twitch.getTopStreams(headers: authStore.headersTwitch, cursor: _topStreamsCurrentCursor);
|
||||
|
||||
if (newTopStreams != null) {
|
||||
if (_topStreamsCurrentCursor == null) {
|
||||
_topStreams = ObservableList.of(newTopStreams['streams']);
|
||||
} else {
|
||||
_topStreams.addAll(newTopStreams['streams']);
|
||||
}
|
||||
_topStreamsCurrentCursor = newTopStreams['cursor'];
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
/// Resets the cursor and then fetches the top streams.
|
||||
@action
|
||||
Future<void> refresh() async {
|
||||
_topStreamsCurrentCursor = null;
|
||||
|
||||
await getTopStreams();
|
||||
}
|
||||
}
|
||||
48
lib/screens/stream_list/streams_top/top_streams_store.g.dart
Normal file
48
lib/screens/stream_list/streams_top/top_streams_store.g.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'top_streams_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 _$TopStreamsStore on _TopStreamsStoreBase, Store {
|
||||
final _$_topStreamsAtom = Atom(name: '_TopStreamsStoreBase._topStreams');
|
||||
|
||||
@override
|
||||
ObservableList<StreamTwitch> get _topStreams {
|
||||
_$_topStreamsAtom.reportRead();
|
||||
return super._topStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
set _topStreams(ObservableList<StreamTwitch> value) {
|
||||
_$_topStreamsAtom.reportWrite(value, super._topStreams, () {
|
||||
super._topStreams = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$getTopStreamsAsyncAction =
|
||||
AsyncAction('_TopStreamsStoreBase.getTopStreams');
|
||||
|
||||
@override
|
||||
Future<void> getTopStreams() {
|
||||
return _$getTopStreamsAsyncAction.run(() => super.getTopStreams());
|
||||
}
|
||||
|
||||
final _$refreshAsyncAction = AsyncAction('_TopStreamsStoreBase.refresh');
|
||||
|
||||
@override
|
||||
Future<void> refresh() {
|
||||
return _$refreshAsyncAction.run(() => super.refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ void main() {
|
|||
''';
|
||||
|
||||
final decoded = jsonDecode(sampleStream);
|
||||
final stream = Stream.fromJson(decoded);
|
||||
final stream = StreamTwitch.fromJson(decoded);
|
||||
|
||||
expect(stream.id, '43809710301');
|
||||
expect(stream.userId, '71092938');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue