Separate sub emotes in emote menu (#142)

Closes #142
This commit is contained in:
Tommy Chow 2022-07-03 05:43:29 -04:00
parent c911767008
commit b9ef4661cf
7 changed files with 135 additions and 85 deletions

View file

@ -264,6 +264,7 @@ class Emote {
final bool zeroWidth;
final String url;
final EmoteType type;
final String? ownerId;
const Emote({
required this.name,
@ -272,6 +273,7 @@ class Emote {
required this.zeroWidth,
required this.url,
required this.type,
this.ownerId,
});
factory Emote.fromTwitch(EmoteTwitch emote, EmoteType type) => Emote(
@ -279,6 +281,7 @@ class Emote {
zeroWidth: false,
url: 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0',
type: type,
ownerId: emote.ownerId,
);
factory Emote.fromBTTV(EmoteBTTV emote, EmoteType type) => Emote(

View file

@ -131,6 +131,7 @@ Emote _$EmoteFromJson(Map<String, dynamic> json) => Emote(
zeroWidth: json['zeroWidth'] as bool,
url: json['url'] as String,
type: $enumDecode(_$EmoteTypeEnumMap, json['type']),
ownerId: json['ownerId'] as String?,
);
Map<String, dynamic> _$EmoteToJson(Emote instance) => <String, dynamic>{
@ -140,6 +141,7 @@ Map<String, dynamic> _$EmoteToJson(Emote instance) => <String, dynamic>{
'zeroWidth': instance.zeroWidth,
'url': instance.url,
'type': _$EmoteTypeEnumMap[instance.type],
'ownerId': instance.ownerId,
};
const _$EmoteTypeEnumMap = {

View file

@ -81,7 +81,7 @@ class _EmoteMenuState extends State<EmoteMenu> {
Observer(
builder: (_) => EmoteMenuPanel(
chatStore: widget.chatStore,
emotes: widget.chatStore.assetsStore.userEmoteToObject.values.toList(),
twitchEmotes: widget.chatStore.assetsStore.userEmoteSectionToEmotes,
),
),
Observer(

View file

@ -6,78 +6,72 @@ import 'package:frosty/widgets/section_header.dart';
class EmoteMenuPanel extends StatelessWidget {
final ChatStore chatStore;
final List<Emote> emotes;
final List<Emote>? emotes;
final Map<String, List<Emote>>? twitchEmotes;
const EmoteMenuPanel({
Key? key,
required this.chatStore,
required this.emotes,
this.emotes,
this.twitchEmotes,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final globalEmotes = emotes
.where((emote) =>
emote.type == EmoteType.twitchGlobal ||
emote.type == EmoteType.bttvGlobal ||
emote.type == EmoteType.ffzGlobal ||
emote.type == EmoteType.sevenTVGlobal)
.toList();
if (emotes != null) {
final globalEmotes =
emotes!.where((emote) => emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.ffzGlobal || emote.type == EmoteType.sevenTVGlobal).toList();
final channelEmotes = emotes
.where((emote) =>
emote.type == EmoteType.twitchChannel ||
emote.type == EmoteType.bttvChannel ||
emote.type == EmoteType.bttvShared ||
emote.type == EmoteType.ffzChannel ||
emote.type == EmoteType.sevenTVChannel)
.toList();
final channelEmotes = emotes!
.where((emote) =>
emote.type == EmoteType.bttvChannel ||
emote.type == EmoteType.bttvShared ||
emote.type == EmoteType.ffzChannel ||
emote.type == EmoteType.sevenTVChannel)
.toList();
final subEmotes = emotes.where((emote) => emote.type == EmoteType.twitchSub).toList();
final miscEmotes = emotes.where((emote) => emote.type == EmoteType.twitchUnlocked).toList();
return CustomScrollView(
slivers: [
if (globalEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader(
'Global Emotes',
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
return CustomScrollView(
slivers: [
if (globalEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader(
'Global Emotes',
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
),
),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: globalEmotes,
),
EmoteMenuSection(
chatStore: chatStore,
emotes: globalEmotes,
),
],
if (channelEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader('Channel Emotes'),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: channelEmotes,
),
],
],
if (channelEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader('Channel Emotes'),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: channelEmotes,
),
);
} else {
return CustomScrollView(
slivers: [
for (final entry in twitchEmotes!.entries) ...[
SliverToBoxAdapter(
child: SectionHeader(
entry.key,
padding: entry.key == 'Global Emotes' ? const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0) : null,
),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: entry.value,
),
]
],
if (subEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader('Subscribed Emotes'),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: subEmotes,
),
],
if (miscEmotes.isNotEmpty) ...[
const SliverToBoxAdapter(
child: SectionHeader('Unlocked Emotes'),
),
EmoteMenuSection(
chatStore: chatStore,
emotes: miscEmotes,
),
],
],
);
);
}
}
}

View file

@ -43,8 +43,14 @@ abstract class ChatAssetsStoreBase with Store {
/// The emotes that are "owned" and may be used by the current user.
@readonly
// ignore: prefer_final_fields
var _userEmoteToObject = <String, Emote>{};
/// The Twitch emote types mapped to their emotes (i.e., global, specific channels, unlocked).
@readonly
// ignore: prefer_final_fields
var _userEmoteSectionToEmotes = <String, List<Emote>>{};
/// The map of user IDs to their FFZ badges.
@readonly
var _userToFFZBadges = <String, List<Badge>>{};
@ -91,8 +97,8 @@ abstract class ChatAssetsStoreBase with Store {
// Retrieve the instance that will allow us to retrieve local search history.
final prefs = await SharedPreferences.getInstance();
_recentEmotes = prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ??
ObservableList<Emote>();
_recentEmotes =
prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ?? ObservableList<Emote>();
_disposeReaction = autorun((_) {
if (_recentEmotes.length > 48) _recentEmotes.removeLast();
@ -167,10 +173,38 @@ abstract class ChatAssetsStoreBase with Store {
required List<String> emoteSets,
required Map<String, String> headers,
required Function onError,
}) =>
Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)))
.then((emotes) => emotes.expand((list) => list).toList())
.then((userEmotes) => _userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable());
}) async {
final userEmotes = await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)));
for (final emoteSet in userEmotes) {
if (emoteSet.isNotEmpty) {
if (emoteSet.first.type == EmoteType.twitchSub) {
final owner = await twitchApi.getUser(id: emoteSet.first.ownerId, headers: headers);
_userEmoteSectionToEmotes.update(
owner.displayName,
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
ifAbsent: () => emoteSet,
);
} else if (emoteSet.first.type == EmoteType.twitchGlobal) {
_userEmoteSectionToEmotes.update(
'Global Emotes',
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
ifAbsent: () => emoteSet,
);
} else if (emoteSet.first.type == EmoteType.twitchUnlocked) {
_userEmoteSectionToEmotes.update(
'Unlocked Emotes',
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
ifAbsent: () => emoteSet,
);
}
for (final emote in emoteSet) {
_userEmoteToObject[emote.name] = emote;
}
}
}
}
void dispose() => _disposeReaction();
}

View file

@ -85,6 +85,26 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
});
}
late final _$_userEmoteSectionToEmotesAtom = Atom(
name: 'ChatAssetsStoreBase._userEmoteSectionToEmotes', context: context);
Map<String, List<Emote>> get userEmoteSectionToEmotes {
_$_userEmoteSectionToEmotesAtom.reportRead();
return super._userEmoteSectionToEmotes;
}
@override
Map<String, List<Emote>> get _userEmoteSectionToEmotes =>
userEmoteSectionToEmotes;
@override
set _userEmoteSectionToEmotes(Map<String, List<Emote>> value) {
_$_userEmoteSectionToEmotesAtom
.reportWrite(value, super._userEmoteSectionToEmotes, () {
super._userEmoteSectionToEmotes = value;
});
}
late final _$_userToFFZBadgesAtom =
Atom(name: 'ChatAssetsStoreBase._userToFFZBadges', context: context);
@ -179,6 +199,18 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
return _$initAsyncAction.run(() => super.init());
}
late final _$userEmotesFutureAsyncAction =
AsyncAction('ChatAssetsStoreBase.userEmotesFuture', context: context);
@override
Future<void> userEmotesFuture(
{required List<String> emoteSets,
required Map<String, String> headers,
required Function onError}) {
return _$userEmotesFutureAsyncAction.run(() => super.userEmotesFuture(
emoteSets: emoteSets, headers: headers, onError: onError));
}
late final _$ChatAssetsStoreBaseActionController =
ActionController(name: 'ChatAssetsStoreBase', context: context);
@ -231,21 +263,6 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
}
}
@override
Future<void> userEmotesFuture(
{required List<String> emoteSets,
required Map<String, String> headers,
required Function onError}) {
final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction(
name: 'ChatAssetsStoreBase.userEmotesFuture');
try {
return super.userEmotesFuture(
emoteSets: emoteSets, headers: headers, onError: onError);
} finally {
_$ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
}
}
@override
String toString() {
return '''

View file

@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
class SectionHeader extends StatelessWidget {
final String text;
final EdgeInsets padding;
final EdgeInsets? padding;
final double? fontSize;
final bool showDivider;
const SectionHeader(
this.text, {
Key? key,
this.padding = const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
this.padding,
this.fontSize,
this.showDivider = false,
}) : super(key: key);
@ -17,7 +17,7 @@ class SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
padding: padding ?? const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [