diff --git a/lib/api/bttv_api.dart b/lib/api/bttv_api.dart index 7dab6c23..8c5e837c 100644 --- a/lib/api/bttv_api.dart +++ b/lib/api/bttv_api.dart @@ -11,7 +11,7 @@ class BTTV { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteBTTVGlobal.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteBTTVGlobal.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index b6e9d677..6f035f30 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -11,7 +11,7 @@ class FFZ { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { @@ -31,7 +31,7 @@ class FFZ { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { diff --git a/lib/api/irc_api.dart b/lib/api/irc_api.dart index 393ef977..ab316b63 100644 --- a/lib/api/irc_api.dart +++ b/lib/api/irc_api.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:frosty/models/irc.dart'; class IRC { @@ -97,11 +97,15 @@ class IRC { span.add( WidgetSpan( alignment: PlaceholderAlignment.middle, - child: CachedNetworkImage( - imageUrl: badgeUrl, - placeholder: (context, url) => const SizedBox(), - fadeInDuration: const Duration(seconds: 0), - height: 20, + child: Tooltip( + message: badge, + preferBelow: false, + child: CachedNetworkImage( + imageUrl: badgeUrl, + placeholder: (context, url) => const SizedBox(), + fadeInDuration: const Duration(seconds: 0), + height: 20, + ), ), ), ); @@ -153,11 +157,15 @@ class IRC { span.add( WidgetSpan( alignment: PlaceholderAlignment.middle, - child: CachedNetworkImage( - imageUrl: emoteUrl, - placeholder: (context, url) => const SizedBox(), - fadeInDuration: const Duration(seconds: 0), - height: 25, + child: Tooltip( + message: word, + preferBelow: false, + child: CachedNetworkImage( + imageUrl: emoteUrl, + placeholder: (context, url) => const SizedBox(), + fadeInDuration: const Duration(seconds: 0), + height: 25, + ), ), ), ); diff --git a/lib/api/seventv_api.dart b/lib/api/seventv_api.dart index 13041450..8d25c544 100644 --- a/lib/api/seventv_api.dart +++ b/lib/api/seventv_api.dart @@ -12,7 +12,7 @@ class SevenTV { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { @@ -31,7 +31,7 @@ class SevenTV { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 6cd6d247..6b674de5 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -17,7 +17,7 @@ class Twitch { if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; - final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { @@ -37,7 +37,7 @@ class Twitch { if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; - final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); final emoteToUrl = {}; for (final emote in emotes) { @@ -51,44 +51,39 @@ class Twitch { } /// Returns a map of global Twitch badges to their URL. - static Future?> getBadgesGlobal({required Map? headers}) async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/badges/global'); - final response = await http.get(url, headers: headers); + static Future?> getBadgesGlobal() async { + final url = Uri.parse('https://badges.twitch.tv/v1/badges/global/display'); + final response = await http.get(url); if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); + final result = {}; - final badgeToUrl = {}; - for (final badge in badges) { - for (final badgeVersion in badge.versions) { - badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; - } - } + final decoded = jsonDecode(response.body)['badge_sets'] as Map; - return badgeToUrl; + // TODO: Figure out cleaner way to decode badge JSON. + decoded.forEach((id, versions) => + (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo).imageUrl4x)); + + return result; } else { debugPrint('Failed to get global Twitch badges. Error code: ${response.statusCode}'); } } /// Returns a map of a channel's Twitch badges to their URL. - static Future?> getBadgesChannel({required String id, required Map? headers}) async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/badges?broadcaster_id=$id'); - final response = await http.get(url, headers: headers); + static Future?> getBadgesChannel({required String id}) async { + final url = Uri.parse('https://badges.twitch.tv/v1/badges/channels/$id/display'); + final response = await http.get(url); if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); + final result = {}; - final badgeToUrl = {}; - for (final badge in badges) { - for (final badgeVersion in badge.versions) { - badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; - } - } + final decoded = jsonDecode(response.body)['badge_sets'] as Map; - return badgeToUrl; + decoded.forEach((id, versions) => + (versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = BadgeInfoTwitch.fromJson(badgeInfo).imageUrl4x)); + + return result; } else { debugPrint('Failed to get Twitch badges for id: $id. Error code: ${response.statusCode}'); } diff --git a/lib/models/badges.dart b/lib/models/badges.dart index 8662c295..be8ada6c 100644 --- a/lib/models/badges.dart +++ b/lib/models/badges.dart @@ -3,9 +3,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'badges.g.dart'; // Twitch Badges -@JsonSerializable(createToJson: false) -class BadgeImagesTwitch { - final String id; +@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) +class BadgeInfoTwitch { @JsonKey(name: 'image_url_1x') final String imageUrl1x; @JsonKey(name: 'image_url_2x') @@ -13,25 +12,20 @@ class BadgeImagesTwitch { @JsonKey(name: 'image_url_4x') final String imageUrl4x; - const BadgeImagesTwitch( - this.id, + final String description; + final String title; + final String clickAction; + final String clickUrl; + + const BadgeInfoTwitch( this.imageUrl1x, this.imageUrl2x, this.imageUrl4x, + this.description, + this.title, + this.clickAction, + this.clickUrl, ); - factory BadgeImagesTwitch.fromJson(Map json) => _$BadgeImagesTwitchFromJson(json); -} - -@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) -class BadgesTwitch { - final String setId; - final List versions; - - const BadgesTwitch( - this.setId, - this.versions, - ); - - factory BadgesTwitch.fromJson(Map json) => _$BadgesTwitchFromJson(json); + factory BadgeInfoTwitch.fromJson(Map json) => _$BadgeInfoTwitchFromJson(json); } diff --git a/lib/models/badges.g.dart b/lib/models/badges.g.dart index 411bf390..ffa966ea 100644 --- a/lib/models/badges.g.dart +++ b/lib/models/badges.g.dart @@ -6,17 +6,13 @@ part of 'badges.dart'; // JsonSerializableGenerator // ************************************************************************** -BadgeImagesTwitch _$BadgeImagesTwitchFromJson(Map json) => - BadgeImagesTwitch( - json['id'] as String, +BadgeInfoTwitch _$BadgeInfoTwitchFromJson(Map json) => + BadgeInfoTwitch( json['image_url_1x'] as String, json['image_url_2x'] as String, json['image_url_4x'] as String, - ); - -BadgesTwitch _$BadgesTwitchFromJson(Map json) => BadgesTwitch( - json['set_id'] as String, - (json['versions'] as List) - .map((e) => BadgeImagesTwitch.fromJson(e as Map)) - .toList(), + json['description'] as String, + json['title'] as String, + json['click_action'] as String, + json['click_url'] as String, ); diff --git a/lib/screens/channel/chat/chat_store.dart b/lib/screens/channel/chat/chat_store.dart index afc76d41..4c511ae6 100644 --- a/lib/screens/channel/chat/chat_store.dart +++ b/lib/screens/channel/chat/chat_store.dart @@ -112,7 +112,7 @@ abstract class _ChatStoreBase with Store { if (message.startsWith('@')) { final parsedIRCMessage = IRCMessage.fromString(message); - // Filter messages from any blocked users if not moderating and not the channel owner + // Filter messages from any blocked users if not a moderator or not the channel owner. if (!_userState.mod && channelName != auth.user?.login && auth.blockedUsers.where((blockedUser) => blockedUser.userLogin == parsedIRCMessage.user).isNotEmpty) continue; @@ -228,8 +228,8 @@ abstract class _ChatStoreBase with Store { await BTTV.getEmotesChannel(id: channelInfo.id), await Twitch.getEmotesGlobal(headers: auth.headersTwitch), await Twitch.getEmotesChannel(id: channelInfo.id, headers: auth.headersTwitch), - await Twitch.getBadgesGlobal(headers: auth.headersTwitch), - await Twitch.getBadgesChannel(id: channelInfo.id, headers: auth.headersTwitch), + await Twitch.getBadgesGlobal(), + await Twitch.getBadgesChannel(id: channelInfo.id), await SevenTV.getEmotesGlobal(), await SevenTV.getEmotesChannel(user: channelInfo.login) ]; diff --git a/test/badge_test.dart b/test/badge_test.dart deleted file mode 100644 index 4e7ee520..00000000 --- a/test/badge_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "dart:convert"; -import "package:flutter_test/flutter_test.dart"; -import 'package:frosty/models/badges.dart'; - -void main() { - group('Twitch', () { - test('badge should parse correctly', () { - const sampleJson = '''{ - "set_id": "vip", - "versions": [ - { - "id": "1", - "image_url_1x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/1", - "image_url_2x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2", - "image_url_4x": "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/3" - } - ] - }'''; - - final decoded = jsonDecode(sampleJson); - final badge = BadgesTwitch.fromJson(decoded); - - expect(badge.setId, "vip"); - expect(badge.versions.first.id, "1"); - expect(badge.versions.length, 1); - expect(badge.versions.first.id, "1"); - expect(badge.versions.first.imageUrl1x, "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/1"); - expect(badge.versions.first.imageUrl2x, "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2"); - expect(badge.versions.first.imageUrl4x, "https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/3"); - }); - }); -}