diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 97b48f23..d9888365 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -326,6 +326,22 @@ class TwitchApi { } } + /// Returns a [CategoriesTwitch] object containing the category info corresponding to the provided [gameId]. + Future getCategory({ + required Map headers, + required String gameId, + }) async { + final url = Uri.parse('https://api.twitch.tv/helix/games?id=$gameId'); + + final response = await _client.get(url, headers: headers); + final decoded = jsonDecode(response.body); + if (response.statusCode == 200) { + return CategoriesTwitch.fromJson(decoded); + } else { + return Future.error('Failed to get category: ${decoded['message']}'); + } + } + /// Returns a [CategoriesTwitch] containing up to 20 categories/games closest matching the [query] and a cursor for further requests. Future searchCategories({ required Map headers, diff --git a/lib/models/category.dart b/lib/models/category.dart index fdaca1e2..34cb2c9c 100644 --- a/lib/models/category.dart +++ b/lib/models/category.dart @@ -20,7 +20,7 @@ class CategoryTwitch { @JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) class CategoriesTwitch { final List data; - final Map pagination; + final Map? pagination; const CategoriesTwitch( this.data, diff --git a/lib/models/category.g.dart b/lib/models/category.g.dart index 13ba544b..c46c36ab 100644 --- a/lib/models/category.g.dart +++ b/lib/models/category.g.dart @@ -18,5 +18,7 @@ CategoriesTwitch _$CategoriesTwitchFromJson(Map json) => (json['data'] as List) .map((e) => CategoryTwitch.fromJson(e as Map)) .toList(), - Map.from(json['pagination'] as Map), + (json['pagination'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), ); diff --git a/lib/screens/home/stores/categories_store.dart b/lib/screens/home/stores/categories_store.dart index 159cb436..e00100af 100644 --- a/lib/screens/home/stores/categories_store.dart +++ b/lib/screens/home/stores/categories_store.dart @@ -5,6 +5,7 @@ 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; @@ -68,7 +69,7 @@ abstract class _CategoriesStoreBase with Store { } else { _categories.addAll(result.data); } - _categoriesCursor = result.pagination['cursor']; + _categoriesCursor = result.pagination?['cursor']; _error = null; } on SocketException { diff --git a/lib/screens/home/top/category_streams.dart b/lib/screens/home/top/category_streams.dart index acda2b40..5bc4d42c 100644 --- a/lib/screens/home/top/category_streams.dart +++ b/lib/screens/home/top/category_streams.dart @@ -95,6 +95,7 @@ class CategoryStreams extends StatelessWidget { width: thumbnailWidth, height: thumbnailHeight, showUptime: context.read().showThumbnailUptime, + showCategory: false, ), ); }, diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index 884532b8..8b50edae 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -1,13 +1,17 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/constants/constants.dart'; +import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; +import 'package:frosty/screens/home/top/category_streams.dart'; import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/profile_picture.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; /// A tappable card widget that displays a stream's thumbnail and details. class StreamCard extends StatelessWidget { @@ -16,6 +20,7 @@ class StreamCard extends StatelessWidget { final int width; final int height; final bool showUptime; + final bool showCategory; const StreamCard({ Key? key, @@ -24,6 +29,7 @@ class StreamCard extends StatelessWidget { required this.width, required this.height, required this.showUptime, + this.showCategory = true, }) : super(key: key); @override @@ -105,7 +111,10 @@ class StreamCard extends StatelessWidget { preferBelow: false, child: Text( streamerName, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), overflow: TextOverflow.ellipsis, ), ), @@ -120,14 +129,50 @@ class StreamCard extends StatelessWidget { child: Text( streamInfo.title.trim(), overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + color: DefaultTextStyle.of(context).style.color?.withOpacity(0.8), + ), ), ), const SizedBox(height: 5.0), - Text(streamInfo.gameName), - const SizedBox(height: 5.0), + if (showCategory) ...[ + InkWell( + child: Text( + streamInfo.gameName, + style: TextStyle( + fontWeight: FontWeight.w300, + color: DefaultTextStyle.of(context).style.color?.withOpacity(0.8), + ), + ), + onTap: () async { + final category = await context + .read() + .getCategory(headers: context.read().headersTwitch, gameId: streamInfo.gameId); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryStreams( + listStore: ListStore( + twitchApi: context.read(), + authStore: context.read(), + listType: ListType.category, + categoryInfo: category.data.first, + ), + ), + ), + ); + }, + ), + const SizedBox(height: 5.0), + ], Text( '${NumberFormat().format(streamInfo.viewerCount)} viewers', - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w300, + color: DefaultTextStyle.of(context).style.color?.withOpacity(0.8), + ), ), ], ),