diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 6921eae0..8f043bcf 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -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'); } diff --git a/lib/main.dart b/lib/main.dart index 90ba3a9a..53f4808c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); + return Home( - streamListStore: StreamListStore(authStore: context.read()), homeStore: HomeStore(), + topStreamsStore: TopStreamsStore(authStore: authStore), + followedStreamsStore: FollowedStreamsStore(authStore: authStore), + categoriesStore: CategoriesStore(authStore: authStore), + searchStore: SearchStore(authStore: authStore), ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/models/stream.dart b/lib/models/stream.dart index c52815e1..57cfe144 100644 --- a/lib/models/stream.dart +++ b/lib/models/stream.dart @@ -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? 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 json) => _$StreamFromJson(json); + factory StreamTwitch.fromJson(Map json) => _$StreamTwitchFromJson(json); } diff --git a/lib/models/stream.g.dart b/lib/models/stream.g.dart index 2d5fe566..96a87042 100644 --- a/lib/models/stream.g.dart +++ b/lib/models/stream.g.dart @@ -6,7 +6,7 @@ part of 'stream.dart'; // JsonSerializableGenerator // ************************************************************************** -Stream _$StreamFromJson(Map json) => Stream( +StreamTwitch _$StreamTwitchFromJson(Map json) => StreamTwitch( json['id'] as String, json['user_id'] as String, json['user_login'] as String, diff --git a/lib/screens/categories/categories.dart b/lib/screens/categories/categories.dart index c10c7bc1..ffb5929f 100644 --- a/lib/screens/categories/categories.dart +++ b/lib/screens/categories/categories.dart @@ -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()); 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]); }, ); }, diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 702a2300..897ce32f 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -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 { - final _textController = TextEditingController(); - @override Widget build(BuildContext context) { debugPrint('build home'); @@ -43,7 +45,7 @@ class _HomeState extends State { 'Search', ]; return Text( - titles[widget.homeStore.selectedIndex], + titles[homeStore.selectedIndex], ); }, ), @@ -66,19 +68,12 @@ class _HomeState extends State { body: Observer( builder: (_) { return IndexedStack( - index: widget.homeStore.selectedIndex, + index: homeStore.selectedIndex, children: [ - if (context.read().isLoggedIn) - StreamList( - category: StreamCategory.followed, - streamListStore: widget.streamListStore, - ), - StreamList( - category: StreamCategory.top, - streamListStore: widget.streamListStore, - ), - const Categories(), - const Search(), + if (context.read().isLoggedIn) FollowedStreams(store: followedStreamsStore), + TopStreams(store: topStreamsStore), + Categories(store: categoriesStore), + Search(store: searchStore), ], ); }, @@ -106,17 +101,11 @@ class _HomeState extends State { label: 'Search', ), ], - currentIndex: widget.homeStore.selectedIndex, - onTap: widget.homeStore.handleTap, + currentIndex: homeStore.selectedIndex, + onTap: homeStore.handleTap, ); }, ), ); } - - @override - void dispose() { - _textController.dispose(); - super.dispose(); - } } diff --git a/lib/screens/search/search.dart b/lib/screens/search/search.dart index 9ba5c1a0..507648d9 100644 --- a/lib/screens/search/search.dart +++ b/lib/screens/search/search.dart @@ -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 { - late final SearchStore searchStore; - - @override - void initState() { - super.initState(); - searchStore = SearchStore(authStore: context.read()); - } - @override Widget build(BuildContext context) { return Column( @@ -28,7 +23,7 @@ class _SearchState extends State { 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 { ), 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 { @override void dispose() { - searchStore.dispose(); + widget.store.dispose(); super.dispose(); } } diff --git a/lib/screens/stream_list/stream_card.dart b/lib/screens/stream_list/stream_card.dart index a633a73a..6c8e562e 100644 --- a/lib/screens/stream_list/stream_card.dart +++ b/lib/screens/stream_list/stream_card.dart @@ -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); diff --git a/lib/screens/stream_list/stream_list.dart b/lib/screens/stream_list/stream_list.dart deleted file mode 100644 index d4a8b95b..00000000 --- a/lib/screens/stream_list/stream_list.dart +++ /dev/null @@ -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]); - }, - ); - }, - ), - ); - } -} diff --git a/lib/screens/stream_list/stream_list_store.dart b/lib/screens/stream_list/stream_list_store.dart deleted file mode 100644 index 0e50f098..00000000 --- a/lib/screens/stream_list/stream_list_store.dart +++ /dev/null @@ -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(); - - /// The list of the fetched followed streams. - @observable - var _followedStreams = ObservableList(); - - /// 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 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 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 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 } diff --git a/lib/screens/stream_list/stream_list_store.g.dart b/lib/screens/stream_list/stream_list_store.g.dart deleted file mode 100644 index 3638b086..00000000 --- a/lib/screens/stream_list/stream_list_store.g.dart +++ /dev/null @@ -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 get _topStreams { - _$_topStreamsAtom.reportRead(); - return super._topStreams; - } - - @override - set _topStreams(ObservableList value) { - _$_topStreamsAtom.reportWrite(value, super._topStreams, () { - super._topStreams = value; - }); - } - - final _$_followedStreamsAtom = Atom(name: '_StreamListBase._followedStreams'); - - @override - ObservableList get _followedStreams { - _$_followedStreamsAtom.reportRead(); - return super._followedStreams; - } - - @override - set _followedStreams(ObservableList value) { - _$_followedStreamsAtom.reportWrite(value, super._followedStreams, () { - super._followedStreams = value; - }); - } - - final _$refreshAsyncAction = AsyncAction('_StreamListBase.refresh'); - - @override - Future refresh({required StreamCategory category}) { - return _$refreshAsyncAction.run(() => super.refresh(category: category)); - } - - final _$getStreamsAsyncAction = AsyncAction('_StreamListBase.getStreams'); - - @override - Future getStreams({required StreamCategory category}) { - return _$getStreamsAsyncAction - .run(() => super.getStreams(category: category)); - } - - @override - String toString() { - return ''' - - '''; - } -} diff --git a/lib/screens/stream_list/streams_followed/followed_streams.dart b/lib/screens/stream_list/streams_followed/followed_streams.dart new file mode 100644 index 00000000..e8b07d4c --- /dev/null +++ b/lib/screens/stream_list/streams_followed/followed_streams.dart @@ -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]); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/stream_list/streams_followed/followed_streams_store.dart b/lib/screens/stream_list/streams_followed/followed_streams_store.dart new file mode 100644 index 00000000..d39f8d45 --- /dev/null +++ b/lib/screens/stream_list/streams_followed/followed_streams_store.dart @@ -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(); + ObservableList 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 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 refresh() async { + _followedStreamsCurrentCursor = null; + + await getFollowedStreams(); + } +} diff --git a/lib/screens/stream_list/streams_followed/followed_streams_store.g.dart b/lib/screens/stream_list/streams_followed/followed_streams_store.g.dart new file mode 100644 index 00000000..087af96a --- /dev/null +++ b/lib/screens/stream_list/streams_followed/followed_streams_store.g.dart @@ -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 get _followedStreams { + _$_followedStreamsAtom.reportRead(); + return super._followedStreams; + } + + @override + set _followedStreams(ObservableList value) { + _$_followedStreamsAtom.reportWrite(value, super._followedStreams, () { + super._followedStreams = value; + }); + } + + final _$getFollowedStreamsAsyncAction = + AsyncAction('_FollowedStreamsStoreBase.getFollowedStreams'); + + @override + Future getFollowedStreams() { + return _$getFollowedStreamsAsyncAction + .run(() => super.getFollowedStreams()); + } + + final _$refreshAsyncAction = AsyncAction('_FollowedStreamsStoreBase.refresh'); + + @override + Future refresh() { + return _$refreshAsyncAction.run(() => super.refresh()); + } + + @override + String toString() { + return ''' + + '''; + } +} diff --git a/lib/screens/stream_list/streams_top/top_streams.dart b/lib/screens/stream_list/streams_top/top_streams.dart new file mode 100644 index 00000000..d4d230ab --- /dev/null +++ b/lib/screens/stream_list/streams_top/top_streams.dart @@ -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]); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/stream_list/streams_top/top_streams_store.dart b/lib/screens/stream_list/streams_top/top_streams_store.dart new file mode 100644 index 00000000..c4a95494 --- /dev/null +++ b/lib/screens/stream_list/streams_top/top_streams_store.dart @@ -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(); + ObservableList 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 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 refresh() async { + _topStreamsCurrentCursor = null; + + await getTopStreams(); + } +} diff --git a/lib/screens/stream_list/streams_top/top_streams_store.g.dart b/lib/screens/stream_list/streams_top/top_streams_store.g.dart new file mode 100644 index 00000000..a7a37500 --- /dev/null +++ b/lib/screens/stream_list/streams_top/top_streams_store.g.dart @@ -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 get _topStreams { + _$_topStreamsAtom.reportRead(); + return super._topStreams; + } + + @override + set _topStreams(ObservableList value) { + _$_topStreamsAtom.reportWrite(value, super._topStreams, () { + super._topStreams = value; + }); + } + + final _$getTopStreamsAsyncAction = + AsyncAction('_TopStreamsStoreBase.getTopStreams'); + + @override + Future getTopStreams() { + return _$getTopStreamsAsyncAction.run(() => super.getTopStreams()); + } + + final _$refreshAsyncAction = AsyncAction('_TopStreamsStoreBase.refresh'); + + @override + Future refresh() { + return _$refreshAsyncAction.run(() => super.refresh()); + } + + @override + String toString() { + return ''' + + '''; + } +} diff --git a/test/stream_test.dart b/test/stream_test.dart index 9e1ae8ea..7e9155d4 100644 --- a/test/stream_test.dart +++ b/test/stream_test.dart @@ -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');