From 49b3385c124b285b17c58303e008b03153464e25 Mon Sep 17 00:00:00 2001 From: tommyxchow Date: Fri, 12 Nov 2021 00:04:39 -0500 Subject: [PATCH] Refactor & rename relevant channels to streams --- README.md | 2 +- lib/api/twitch_api.dart | 24 ++-- lib/constants.dart | 53 +------- lib/main.dart | 4 +- lib/screens/home.dart | 24 ++-- lib/stores/channel_list_store.dart | 113 ------------------ lib/stores/channel_list_store.g.dart | 64 ---------- lib/stores/stream_list_store.dart | 113 ++++++++++++++++++ lib/stores/stream_list_store.g.dart | 63 ++++++++++ lib/widgets/channel_list.dart | 34 ------ .../{channel_card.dart => stream_card.dart} | 22 ++-- lib/widgets/stream_list.dart | 32 +++++ test/channel_test.dart | 48 -------- test/stream_test.dart | 48 ++++++++ 14 files changed, 296 insertions(+), 348 deletions(-) delete mode 100644 lib/stores/channel_list_store.dart delete mode 100644 lib/stores/channel_list_store.g.dart create mode 100644 lib/stores/stream_list_store.dart create mode 100644 lib/stores/stream_list_store.g.dart delete mode 100644 lib/widgets/channel_list.dart rename lib/widgets/{channel_card.dart => stream_card.dart} (74%) create mode 100644 lib/widgets/stream_list.dart delete mode 100644 test/channel_test.dart create mode 100644 test/stream_test.dart diff --git a/README.md b/README.md index 8236d6a3..7c2b7da8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A [Twitch](https://www.twitch.tv/) client for iOS and Android, built with [Flutt ## Features -- Browse top and followed channels on Twitch +- Browse top and followed streams on Twitch - Watch live streams with live chat - Support for BTTV, FFZ, and 7TV emotes, including GIFs diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 1b6502fa..fe28f34d 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -141,14 +141,14 @@ class Twitch { return false; } - /// Returns a map with top 10 streamers and a cursor for further requests. - static Future?> getTopChannels({required Map? headers, required String? cursor}) async { + /// Returns a map containing top 20 streams and a cursor for further requests. + static Future?> getTopStreams({required Map? headers, required String? cursor}) async { final Uri uri; if (cursor == null) { - uri = Uri.parse('https://api.twitch.tv/helix/streams?first=10'); + uri = Uri.parse('https://api.twitch.tv/helix/streams'); } else { - uri = Uri.parse('https://api.twitch.tv/helix/streams?first=10&after=$cursor'); + uri = Uri.parse('https://api.twitch.tv/helix/streams?after=$cursor'); } final response = await http.get(uri, headers: headers); @@ -157,20 +157,20 @@ class Twitch { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - return {'channels': data.map((channel) => Stream.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + return {'streams': data.map((stream) => Stream.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']}; } else { - debugPrint('Failed to update top channels'); + debugPrint('Failed to update top streams'); } } - /// Returns a map with the given user's top 10 followed streamers and a cursor for further requests. - static Future?> getFollowedChannels({required String id, required Map? headers, required String? cursor}) async { + /// Returns a map with the given user's top 20 followed streams and a cursor for further requests. + static Future?> getFollowedStreams({required String id, required Map? headers, required String? cursor}) async { final Uri uri; if (cursor == null) { - uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); + uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id'); } else { - uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$cursor'); + uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&after=$cursor'); } final response = await http.get(uri, headers: headers); @@ -179,9 +179,9 @@ class Twitch { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - return {'channels': data.map((channel) => Stream.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + return {'streams': data.map((stream) => Stream.fromJson(stream)).toList(), 'cursor': decoded['pagination']['cursor']}; } else { - debugPrint('Failed to update followed channels'); + debugPrint('Failed to update followed streams'); } } diff --git a/lib/constants.dart b/lib/constants.dart index 6d4e8819..dfbe7eb0 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,54 +1,5 @@ -import 'package:frosty/models/stream.dart'; - +/// Twitch API client ID. const clientId = String.fromEnvironment('CLIENT_ID'); +/// Twitch API client secret. const secret = String.fromEnvironment('SECRET'); - -/// Sample channel objects for testing/preview purposes. -const sampleChannels = [ - Stream( - id: '', - userId: '', - userLogin: 'xqcow', - userName: 'xQcOW', - gameId: '', - gameName: 'VALORANT', - type: 'live', - title: 'TITANIC MAN WITH UNREAL UNMATCHED UNBELIEVABLE STRENGHT AND APTITUDE ACCOMPLISHES ACHIEVEMENTS DEEMED IMPOSSIBLE BY THE PUBLIC (GONE WRONG)', - viewerCount: 76422, - startedAt: '', - language: 'en', - thumbnailUrl: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg', - tagIds: [], - isMature: false), - Stream( - id: '', - userId: '', - userLogin: 'lirik', - userName: 'Lirik', - gameId: '', - gameName: 'Just Chatting', - type: 'live', - title: 'LOL', - viewerCount: 11933, - startedAt: '', - language: 'en', - thumbnailUrl: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_lirik-{width}x{height}.jpg', - tagIds: [], - isMature: false), - Stream( - id: '', - userId: '', - userLogin: 'mizkif', - userName: 'Mizkif', - gameId: '', - gameName: 'Just Chatting', - type: 'live', - title: 'DRAMA DRAMA DRAMA', - viewerCount: 4547, - startedAt: '', - language: 'en', - thumbnailUrl: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_mizkif-{width}x{height}.jpg', - tagIds: [], - isMature: false) -]; diff --git a/lib/main.dart b/lib/main.dart index 2a8fff4e..4a4b2527 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:frosty/screens/home.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:frosty/stores/channel_list_store.dart'; +import 'package:frosty/stores/stream_list_store.dart'; import 'package:frosty/stores/home_store.dart'; import 'package:frosty/stores/settings_store.dart'; import 'package:provider/provider.dart'; @@ -39,7 +39,7 @@ class MyApp extends StatelessWidget { builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Home( - channelListStore: ChannelListStore(authStore: context.read()), + streamListStore: StreamListStore(authStore: context.read()), homeStore: HomeStore(), ); } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index e3ffda5c..169f02c6 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -3,21 +3,21 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/search.dart'; import 'package:frosty/screens/settings.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:frosty/stores/channel_list_store.dart'; +import 'package:frosty/stores/stream_list_store.dart'; import 'package:frosty/stores/home_store.dart'; import 'package:frosty/stores/settings_store.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; -import '../widgets/channel_list.dart'; +import '../widgets/stream_list.dart'; class Home extends StatefulWidget { final HomeStore homeStore; - final ChannelListStore channelListStore; + final StreamListStore streamListStore; const Home({ Key? key, required this.homeStore, - required this.channelListStore, + required this.streamListStore, }) : super(key: key); @override @@ -30,8 +30,8 @@ class _HomeState extends State { @override Widget build(BuildContext context) { final titles = [ - if (context.read().isLoggedIn) 'Followed Channels', - 'Top Channels', + if (context.read().isLoggedIn) 'Followed Streams', + 'Top Streams', 'Categories', 'Search', ]; @@ -78,13 +78,13 @@ class _HomeState extends State { index: widget.homeStore.selectedIndex, children: [ if (context.read().isLoggedIn) - ChannelList( - category: ChannelCategory.followed, - channelListStore: widget.channelListStore, + StreamList( + category: StreamCategory.followed, + streamListStore: widget.streamListStore, ), - ChannelList( - category: ChannelCategory.top, - channelListStore: widget.channelListStore, + StreamList( + category: StreamCategory.top, + streamListStore: widget.streamListStore, ), const Center( child: Text('Games'), diff --git a/lib/stores/channel_list_store.dart b/lib/stores/channel_list_store.dart deleted file mode 100644 index 51f3af0d..00000000 --- a/lib/stores/channel_list_store.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:frosty/api/twitch_api.dart'; -import 'package:mobx/mobx.dart'; -import 'package:frosty/stores/auth_store.dart'; -import 'package:frosty/models/stream.dart'; - -part 'channel_list_store.g.dart'; - -// TODO: Fix followed list refresh when log in. -class ChannelListStore = _ChannelListBase with _$ChannelListStore; - -abstract class _ChannelListBase with Store { - /// The list of the fetched top channels - @observable - var _topChannels = ObservableList(); - - /// The list of the fetched followed channels - @observable - var _followedChannels = ObservableList(); - - /// The loading status for pagination. - bool _isLoading = false; - - /// The pagination cursor for top channels. - String? _topChannelsCurrentCursor; - - /// The pagination cursor for followed channels. - String? _followedChannelsCurrentCursor; - - /// The authentication store. - final AuthStore authStore; - - _ChannelListBase({required this.authStore}) { - getChannels(category: ChannelCategory.top); - - if (authStore.user?.id != null) { - getChannels(category: ChannelCategory.followed); - } - } - - /// Returns the appropriate channels the [category]. - ObservableList channels({required ChannelCategory category}) { - switch (category) { - case ChannelCategory.top: - return _topChannels; - case ChannelCategory.followed: - return _followedChannels; - } - } - - /// Returns whether or not there are more channels for the [category]. - bool hasMore({required ChannelCategory category}) { - switch (category) { - case ChannelCategory.top: - return _isLoading == false && _topChannelsCurrentCursor != null; - case ChannelCategory.followed: - return _isLoading == false && _followedChannelsCurrentCursor != null; - } - } - - /// Resets the cursor and then fetches the channels for the [category]. - @action - Future refresh({required ChannelCategory category}) async { - switch (category) { - case ChannelCategory.top: - _topChannelsCurrentCursor = null; - break; - case ChannelCategory.followed: - _followedChannelsCurrentCursor = null; - break; - } - - await getChannels(category: category); - } - - /// Fetches the channels for the [category] based on the current cursor. - @action - Future getChannels({required ChannelCategory category}) async { - _isLoading = true; - - switch (category) { - case ChannelCategory.top: - final newTopChannels = await Twitch.getTopChannels(headers: authStore.headersTwitch, cursor: _topChannelsCurrentCursor); - - if (newTopChannels != null) { - if (_topChannelsCurrentCursor == null) { - _topChannels = ObservableList.of(newTopChannels['channels']); - } else { - _topChannels.addAll(newTopChannels['channels']); - } - _topChannelsCurrentCursor = newTopChannels['cursor']; - } - break; - case ChannelCategory.followed: - final newFollowedChannels = - await Twitch.getFollowedChannels(id: authStore.user!.id, headers: authStore.headersTwitch, cursor: _followedChannelsCurrentCursor); - - if (newFollowedChannels != null) { - if (_followedChannelsCurrentCursor == null) { - _followedChannels = ObservableList.of(newFollowedChannels['channels']); - } else { - _followedChannels.addAll(newFollowedChannels['channels']); - } - _followedChannelsCurrentCursor = newFollowedChannels['cursor']; - } - break; - } - - _isLoading = false; - } -} - -/// The type of category for channels. -enum ChannelCategory { top, followed } diff --git a/lib/stores/channel_list_store.g.dart b/lib/stores/channel_list_store.g.dart deleted file mode 100644 index a0079f73..00000000 --- a/lib/stores/channel_list_store.g.dart +++ /dev/null @@ -1,64 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'channel_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 _$ChannelListStore on _ChannelListBase, Store { - final _$_topChannelsAtom = Atom(name: '_ChannelListBase._topChannels'); - - @override - ObservableList get _topChannels { - _$_topChannelsAtom.reportRead(); - return super._topChannels; - } - - @override - set _topChannels(ObservableList value) { - _$_topChannelsAtom.reportWrite(value, super._topChannels, () { - super._topChannels = value; - }); - } - - final _$_followedChannelsAtom = - Atom(name: '_ChannelListBase._followedChannels'); - - @override - ObservableList get _followedChannels { - _$_followedChannelsAtom.reportRead(); - return super._followedChannels; - } - - @override - set _followedChannels(ObservableList value) { - _$_followedChannelsAtom.reportWrite(value, super._followedChannels, () { - super._followedChannels = value; - }); - } - - final _$refreshAsyncAction = AsyncAction('_ChannelListBase.refresh'); - - @override - Future refresh({required ChannelCategory category}) { - return _$refreshAsyncAction.run(() => super.refresh(category: category)); - } - - final _$getChannelsAsyncAction = AsyncAction('_ChannelListBase.getChannels'); - - @override - Future getChannels({required ChannelCategory category}) { - return _$getChannelsAsyncAction - .run(() => super.getChannels(category: category)); - } - - @override - String toString() { - return ''' - - '''; - } -} diff --git a/lib/stores/stream_list_store.dart b/lib/stores/stream_list_store.dart new file mode 100644 index 00000000..c9079377 --- /dev/null +++ b/lib/stores/stream_list_store.dart @@ -0,0 +1,113 @@ +import 'package:frosty/api/twitch_api.dart'; +import 'package:mobx/mobx.dart'; +import 'package:frosty/stores/auth_store.dart'; +import 'package:frosty/models/stream.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 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/stores/stream_list_store.g.dart b/lib/stores/stream_list_store.g.dart new file mode 100644 index 00000000..3638b086 --- /dev/null +++ b/lib/stores/stream_list_store.g.dart @@ -0,0 +1,63 @@ +// 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/widgets/channel_list.dart b/lib/widgets/channel_list.dart deleted file mode 100644 index 30541aa4..00000000 --- a/lib/widgets/channel_list.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:frosty/stores/channel_list_store.dart'; -import 'package:frosty/widgets/channel_card.dart'; - -class ChannelList extends StatelessWidget { - final ChannelCategory category; - final ChannelListStore channelListStore; - - const ChannelList({Key? key, required this.category, required this.channelListStore}) : super(key: key); - - @override - Widget build(BuildContext context) { - debugPrint('Build channel list!'); - - return RefreshIndicator( - child: Observer( - builder: (_) { - return ListView.builder( - itemCount: channelListStore.channels(category: category).length, - padding: const EdgeInsets.all(5.0), - itemBuilder: (context, index) { - if (index > channelListStore.channels(category: category).length / 2 && channelListStore.hasMore(category: category)) { - channelListStore.getChannels(category: category); - } - return ChannelCard(channelInfo: channelListStore.channels(category: category).elementAt(index)); - }, - ); - }, - ), - onRefresh: () => channelListStore.refresh(category: category), - ); - } -} diff --git a/lib/widgets/channel_card.dart b/lib/widgets/stream_card.dart similarity index 74% rename from lib/widgets/channel_card.dart rename to lib/widgets/stream_card.dart index d5eaf83b..1a3c68fc 100644 --- a/lib/widgets/channel_card.dart +++ b/lib/widgets/stream_card.dart @@ -4,11 +4,11 @@ import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/video_chat.dart'; import 'package:intl/intl.dart'; -/// A card widget that displays a live channel's thumbnail and details. -class ChannelCard extends StatelessWidget { - final Stream channelInfo; +/// A tappable card widget that displays a stream's thumbnail and details. +class StreamCard extends StatelessWidget { + final Stream streamInfo; - const ChannelCard({Key? key, required this.channelInfo}) : super(key: key); + const StreamCard({Key? key, required this.streamInfo}) : super(key: key); @override Widget build(BuildContext context) { @@ -20,8 +20,8 @@ class ChannelCard extends StatelessWidget { MaterialPageRoute( builder: (context) { return VideoChat( - userLogin: channelInfo.userLogin, - userName: channelInfo.userName, + userLogin: streamInfo.userLogin, + userName: streamInfo.userName, ); }, ), @@ -36,7 +36,7 @@ class ChannelCard extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(left: 10.0), child: CachedNetworkImage( - imageUrl: channelInfo.thumbnailUrl.replaceFirst('-{width}x{height}', '-440x248'), + imageUrl: streamInfo.thumbnailUrl.replaceFirst('-{width}x{height}', '-440x248'), ), ), ), @@ -49,22 +49,22 @@ class ChannelCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - channelInfo.userName, + streamInfo.userName, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 5), SingleChildScrollView( scrollDirection: Axis.horizontal, - child: Text(channelInfo.title.replaceAll('\n', '')), + child: Text(streamInfo.title.replaceAll('\n', '')), ), const SizedBox(height: 5), Text( - channelInfo.gameName, + streamInfo.gameName, style: const TextStyle(fontSize: 12), ), const SizedBox(height: 5), Text( - '${NumberFormat().format(channelInfo.viewerCount)} viewers', + '${NumberFormat().format(streamInfo.viewerCount)} viewers', style: const TextStyle(fontSize: 12), ), ], diff --git a/lib/widgets/stream_list.dart b/lib/widgets/stream_list.dart new file mode 100644 index 00000000..0f1e61a8 --- /dev/null +++ b/lib/widgets/stream_list.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/stores/stream_list_store.dart'; +import 'package:frosty/widgets/stream_card.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( + child: Observer( + builder: (_) { + return ListView.builder( + itemCount: streamListStore.streams(category: category).length, + padding: const EdgeInsets.all(5.0), + 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).elementAt(index)); + }, + ); + }, + ), + onRefresh: () => streamListStore.refresh(category: category), + ); + } +} diff --git a/test/channel_test.dart b/test/channel_test.dart deleted file mode 100644 index 75408de9..00000000 --- a/test/channel_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:convert'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:frosty/models/stream.dart'; - -void main() { - test('Twitch channel should parse', () { - const sampleChannel = ''' - { - "id":"43809710301", - "user_id":"71092938", - "user_login":"xqcow", - "user_name":"xQcOW", - "game_id":"508292", - "game_name":"Bloons TD 6", - "type":"live", - "title":"A shorter title...", - "viewer_count":50051, - "started_at":"2021-09-23T20:06:22Z", - "language":"en", - "thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg", - "tag_ids":[ - "e6bb8b34-4c28-4b5f-94ed-12c1ebf2d0e4", - "6ea6bca4-4712-4ab9-a906-e3336a9d8039", - "6606e54c-f92d-40f6-8257-74977889ccdd" - ], - "is_mature":false - } - '''; - - final decoded = jsonDecode(sampleChannel); - final channel = Stream.fromJson(decoded); - - expect(channel.id, '43809710301'); - expect(channel.userId, '71092938'); - expect(channel.userLogin, 'xqcow'); - expect(channel.userName, 'xQcOW'); - expect(channel.gameId, '508292'); - expect(channel.gameName, 'Bloons TD 6'); - expect(channel.type, 'live'); - expect(channel.title, 'A shorter title...'); - expect(channel.viewerCount, 50051); - expect(channel.startedAt, '2021-09-23T20:06:22Z'); - expect(channel.language, 'en'); - expect(channel.thumbnailUrl, 'https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg'); - expect(channel.tagIds, ["e6bb8b34-4c28-4b5f-94ed-12c1ebf2d0e4", "6ea6bca4-4712-4ab9-a906-e3336a9d8039", "6606e54c-f92d-40f6-8257-74977889ccdd"]); - expect(channel.isMature, false); - }); -} diff --git a/test/stream_test.dart b/test/stream_test.dart new file mode 100644 index 00000000..9e1ae8ea --- /dev/null +++ b/test/stream_test.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:frosty/models/stream.dart'; + +void main() { + test('Twitch stream should parse correctly', () { + const sampleStream = ''' + { + "id":"43809710301", + "user_id":"71092938", + "user_login":"xqcow", + "user_name":"xQcOW", + "game_id":"508292", + "game_name":"Bloons TD 6", + "type":"live", + "title":"A shorter title...", + "viewer_count":50051, + "started_at":"2021-09-23T20:06:22Z", + "language":"en", + "thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg", + "tag_ids":[ + "e6bb8b34-4c28-4b5f-94ed-12c1ebf2d0e4", + "6ea6bca4-4712-4ab9-a906-e3336a9d8039", + "6606e54c-f92d-40f6-8257-74977889ccdd" + ], + "is_mature":false + } + '''; + + final decoded = jsonDecode(sampleStream); + final stream = Stream.fromJson(decoded); + + expect(stream.id, '43809710301'); + expect(stream.userId, '71092938'); + expect(stream.userLogin, 'xqcow'); + expect(stream.userName, 'xQcOW'); + expect(stream.gameId, '508292'); + expect(stream.gameName, 'Bloons TD 6'); + expect(stream.type, 'live'); + expect(stream.title, 'A shorter title...'); + expect(stream.viewerCount, 50051); + expect(stream.startedAt, '2021-09-23T20:06:22Z'); + expect(stream.language, 'en'); + expect(stream.thumbnailUrl, 'https://static-cdn.jtvnw.net/previews-ttv/live_user_xqcow-{width}x{height}.jpg'); + expect(stream.tagIds, ["e6bb8b34-4c28-4b5f-94ed-12c1ebf2d0e4", "6ea6bca4-4712-4ab9-a906-e3336a9d8039", "6606e54c-f92d-40f6-8257-74977889ccdd"]); + expect(stream.isMature, false); + }); +}