From c86b0f8c8f8d6df396b919a0d1a1c8a29372a34e Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Thu, 30 Dec 2021 23:00:03 -0500 Subject: [PATCH] Add support for FFZ dev, bot, and supporter badges --- lib/api/ffz_api.dart | 51 ++++++++++++++++++- lib/models/badges.dart | 43 ++++++++++++++++ lib/models/badges.g.dart | 17 +++++++ lib/models/irc_message.dart | 38 +++++++++++++- .../channel/chat/chat_assets_store.dart | 10 +++- lib/screens/channel/chat/chat_message.dart | 9 ++-- 6 files changed, 159 insertions(+), 9 deletions(-) diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index e8d34f36..7787e231 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; import 'package:http/http.dart' as http; @@ -11,7 +12,7 @@ class FFZ { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)); return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList(); } else { @@ -27,7 +28,7 @@ class FFZ { if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)); return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList(); } else { @@ -35,4 +36,50 @@ class FFZ { return []; } } + + static Future>?> getBadges() async { + final url = Uri.parse('https://api.frankerfacez.com/v1/badges'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + final badges = decoded['badges'] as List; + final users = decoded['users']; + + final badgeObjects = badges.map((badge) => BadgeInfoFFZ.fromJson(badge)).toList(); + + final result = >{}; + final developers = users['1'] as List; + final bots = users['2'] as List; + final supporters = users['3'] as List; + + for (var username in developers) { + final entry = result[username]; + if (entry != null) { + entry.add(badgeObjects[0]); + } else { + result[username] = [badgeObjects[0]]; + } + } + for (var username in bots) { + final entry = result[username]; + if (entry != null) { + entry.add(badgeObjects[1]); + } else { + result[username] = [badgeObjects[1]]; + } + } + for (var username in supporters) { + final entry = result[username]; + if (entry != null) { + entry.add(badgeObjects[2]); + } else { + result[username] = [badgeObjects[2]]; + } + } + + return result; + } + debugPrint('Failed to get FFZ badges'); + } } diff --git a/lib/models/badges.dart b/lib/models/badges.dart index be8ada6c..52d21c22 100644 --- a/lib/models/badges.dart +++ b/lib/models/badges.dart @@ -29,3 +29,46 @@ class BadgeInfoTwitch { factory BadgeInfoTwitch.fromJson(Map json) => _$BadgeInfoTwitchFromJson(json); } + +@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) +class BadgeInfoFFZ { + final int id; + final String name; + final String title; + final int slot; + final String? replaces; + final String color; + final String image; + final BadgeUrlsFFZ urls; + + const BadgeInfoFFZ( + this.id, + this.name, + this.title, + this.slot, + this.replaces, + this.color, + this.image, + this.urls, + ); + + factory BadgeInfoFFZ.fromJson(Map json) => _$BadgeInfoFFZFromJson(json); +} + +@JsonSerializable(createToJson: false) +class BadgeUrlsFFZ { + @JsonKey(name: '1') + final String url1x; + @JsonKey(name: '2') + final String url2x; + @JsonKey(name: '4') + final String url4x; + + const BadgeUrlsFFZ( + this.url1x, + this.url2x, + this.url4x, + ); + + factory BadgeUrlsFFZ.fromJson(Map json) => _$BadgeUrlsFFZFromJson(json); +} diff --git a/lib/models/badges.g.dart b/lib/models/badges.g.dart index ffa966ea..b97ecdc3 100644 --- a/lib/models/badges.g.dart +++ b/lib/models/badges.g.dart @@ -16,3 +16,20 @@ BadgeInfoTwitch _$BadgeInfoTwitchFromJson(Map json) => json['click_action'] as String, json['click_url'] as String, ); + +BadgeInfoFFZ _$BadgeInfoFFZFromJson(Map json) => BadgeInfoFFZ( + json['id'] as int, + json['name'] as String, + json['title'] as String, + json['slot'] as int, + json['replaces'] as String?, + json['color'] as String, + json['image'] as String, + BadgeUrlsFFZ.fromJson(json['urls'] as Map), + ); + +BadgeUrlsFFZ _$BadgeUrlsFFZFromJson(Map json) => BadgeUrlsFFZ( + json['1'] as String, + json['2'] as String, + json['4'] as String, + ); diff --git a/lib/models/irc_message.dart b/lib/models/irc_message.dart index 3f927e32..a25df7d8 100644 --- a/lib/models/irc_message.dart +++ b/lib/models/irc_message.dart @@ -71,7 +71,8 @@ class IRCMessage { List generateSpan({ required TextStyle? style, required Map emoteToObject, - required Map badgeToObject, + required Map twitchBadges, + required Map> ffzBadges, bool hideMessage = false, bool zeroWidthEnabled = false, Timestamp timestamp = Timestamp.none, @@ -103,10 +104,43 @@ class IRCMessage { } // Add any badges to the span. + + // FFZ badges + var ignoreMod = false; + final ffzUserBadges = ffzBadges[user]; + if (ffzUserBadges != null) { + for (final ffzBadge in ffzUserBadges) { + if (ffzBadge.replaces != null) ignoreMod = true; + + span.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Tooltip( + message: ffzBadge.title, + preferBelow: false, + child: ColoredBox( + color: ffzBadge.name == 'bot' ? HexColor.fromHex('#00AD03') : HexColor.fromHex(ffzBadge.color), + child: CachedNetworkImage( + imageUrl: 'https:' + ffzBadge.urls.url4x, + placeholder: (context, url) => const SizedBox(), + fadeInDuration: const Duration(seconds: 0), + height: 20, + ), + ), + ), + ), + ); + span.add(const TextSpan(text: ' ')); + } + } + + // Twitch badges final badges = tags['badges']; if (badges != null) { for (final badge in badges.split(',')) { - final badgeInfo = badgeToObject[badge]; + if (badge == 'moderator/1' && ignoreMod) continue; + + final badgeInfo = twitchBadges[badge]; if (badgeInfo != null) { span.add( WidgetSpan( diff --git a/lib/screens/channel/chat/chat_assets_store.dart b/lib/screens/channel/chat/chat_assets_store.dart index 0d3932f4..0bfe77cb 100644 --- a/lib/screens/channel/chat/chat_assets_store.dart +++ b/lib/screens/channel/chat/chat_assets_store.dart @@ -28,7 +28,10 @@ abstract class _ChatAssetsStoreBase with Store { var _userEmoteToObject = ObservableMap(); /// The map of badges ids to their object representation. - final badgesToObject = {}; + final twitchBadgesToObject = {}; + + /// The map of usernames to their FFZ badges. + final userToFFZBadges = >{}; /// The current index of the emote menu stack. @observable @@ -81,9 +84,12 @@ abstract class _ChatAssetsStoreBase with Store { for (final map in badges) { if (map != null) { - badgesToObject.addAll(map); + twitchBadgesToObject.addAll(map); } } + + final ffzBadges = await FFZ.getBadges(); + if (ffzBadges != null) userToFFZBadges.addAll(ffzBadges); } } diff --git a/lib/screens/channel/chat/chat_message.dart b/lib/screens/channel/chat/chat_message.dart index 90045982..2095802f 100644 --- a/lib/screens/channel/chat/chat_message.dart +++ b/lib/screens/channel/chat/chat_message.dart @@ -33,7 +33,8 @@ class ChatMessage extends StatelessWidget { children: ircMessage.generateSpan( style: DefaultTextStyle.of(context).style, emoteToObject: assetsStore.emoteToObject, - badgeToObject: assetsStore.badgesToObject, + twitchBadges: assetsStore.twitchBadgesToObject, + ffzBadges: assetsStore.userToFFZBadges, zeroWidthEnabled: zeroWidth, timestamp: timestamp, ), @@ -56,7 +57,8 @@ class ChatMessage extends StatelessWidget { children: ircMessage.generateSpan( style: DefaultTextStyle.of(context).style, emoteToObject: assetsStore.emoteToObject, - badgeToObject: assetsStore.badgesToObject, + twitchBadges: assetsStore.twitchBadgesToObject, + ffzBadges: assetsStore.userToFFZBadges, hideMessage: hideMessageIfBanned, zeroWidthEnabled: zeroWidth, timestamp: timestamp, @@ -105,7 +107,8 @@ class ChatMessage extends StatelessWidget { children: ircMessage.generateSpan( style: DefaultTextStyle.of(context).style, emoteToObject: assetsStore.emoteToObject, - badgeToObject: assetsStore.badgesToObject, + twitchBadges: assetsStore.twitchBadgesToObject, + ffzBadges: assetsStore.userToFFZBadges, zeroWidthEnabled: zeroWidth, timestamp: timestamp, ),