Add emote autocomplete & tweak emote menu (#101)
* Add emote search/autocomplete & refactor (#61) - Removed get global emotes future - Added convenience `addEmote` method - Added ability to clear recent emotes - Refactored emote menu * Fix unusable emotes showing on autocomplete (#61) * Add option to disable autocomplete (#61) * Convert autocomplete to builder (#61) * Fix autocomplete empty & add clear dialog (#61) * Fix typo * Fix emote menu removing last word (#61) * Fix emote menu deadzones & use `InkWell` (#61) * Improve emote sizing in autocomplete (#61)
This commit is contained in:
parent
47239870dc
commit
67a1c338ee
13 changed files with 320 additions and 175 deletions
|
|
@ -107,10 +107,7 @@ class Chat extends StatelessWidget {
|
|||
if (chatStore.assetsStore.showEmoteMenu)
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 3,
|
||||
child: EmoteMenu(
|
||||
assetsStore: chatStore.assetsStore,
|
||||
textController: chatStore.textController,
|
||||
),
|
||||
child: EmoteMenu(chatStore: chatStore),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
|
||||
class EmoteMenu extends StatefulWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
|
||||
const EmoteMenu({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -20,7 +18,7 @@ class EmoteMenu extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EmoteMenuState extends State<EmoteMenu> {
|
||||
late final PageController _pageContoller = PageController(initialPage: widget.assetsStore.emoteMenuIndex);
|
||||
late final PageController _pageContoller = PageController(initialPage: widget.chatStore.assetsStore.emoteMenuIndex);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -47,9 +45,9 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
widget.assetsStore.emoteMenuIndex = index;
|
||||
widget.chatStore.assetsStore.emoteMenuIndex = index;
|
||||
},
|
||||
style: index == widget.assetsStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
|
||||
style: index == widget.chatStore.assetsStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
|
||||
child: Text(section),
|
||||
),
|
||||
),
|
||||
|
|
@ -59,39 +57,34 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
onPageChanged: (index) => widget.assetsStore.emoteMenuIndex = index,
|
||||
onPageChanged: (index) => widget.chatStore.assetsStore.emoteMenuIndex = index,
|
||||
controller: _pageContoller,
|
||||
children: [
|
||||
RecentEmotesPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
chatStore: widget.chatStore,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.userEmoteToObject.values.toList(),
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.userEmoteToObject.values.toList(),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.bttvEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.bttvEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.ffzEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.ffzEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
assetsStore: widget.assetsStore,
|
||||
textController: widget.textController,
|
||||
emotes: widget.assetsStore.sevenTVEmotes,
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.sevenTVEmotes,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_section.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class EmoteMenuPanel extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
final List<Emote> emotes;
|
||||
|
||||
const EmoteMenuPanel({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
required this.emotes,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -48,8 +46,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -58,8 +55,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Channel Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: channelEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -68,8 +64,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Subscribed Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: subEmotes,
|
||||
),
|
||||
],
|
||||
|
|
@ -78,8 +73,7 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
child: SectionHeader('Unlocked Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
assetsStore: assetsStore,
|
||||
textController: textController,
|
||||
chatStore: chatStore,
|
||||
emotes: miscEmotes,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,46 +2,38 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmoteMenuSection extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
final ChatStore chatStore;
|
||||
final List<Emote> emotes;
|
||||
|
||||
const EmoteMenuSection({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
required this.emotes,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
textController.text += ' ' + emotes[index].name;
|
||||
assetsStore.recentEmotes.remove(emotes[index]);
|
||||
assetsStore.recentEmotes.insert(0, emotes[index]);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index]),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
|
|
@ -51,8 +43,8 @@ class EmoteMenuSection extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
childCount: emotes.length,
|
||||
),
|
||||
childCount: emotes.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,91 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecentEmotesPanel extends StatelessWidget {
|
||||
final ChatAssetsStore assetsStore;
|
||||
final TextEditingController textController;
|
||||
class RecentEmotesPanel extends StatefulWidget {
|
||||
final ChatStore chatStore;
|
||||
|
||||
const RecentEmotesPanel({
|
||||
Key? key,
|
||||
required this.assetsStore,
|
||||
required this.textController,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RecentEmotesPanel> createState() => _RecentEmotesPanelState();
|
||||
}
|
||||
|
||||
class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
||||
Future<void> _showClearDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear Recent Emotes'),
|
||||
content: const Text('Are you sure you want to clear your recent emotes?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(widget.chatStore.assetsStore.recentEmotes.clear);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Recent Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Recent Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: widget.chatStore.assetsStore.recentEmotes.isEmpty ? null : _showClearDialog,
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...assetsStore.emoteToObject.values, ...assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = widget.chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...widget.chatStore.assetsStore.emoteToObject.values, ...widget.chatStore.assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: matchingEmotes.isNotEmpty
|
||||
? () {
|
||||
textController.text += ' ' + emote.name;
|
||||
assetsStore.recentEmotes.removeWhere((recentEmote) => recentEmote.name == matchingEmotes.first.name);
|
||||
assetsStore.recentEmotes.insert(0, matchingEmotes.first);
|
||||
}
|
||||
: null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
return InkWell(
|
||||
onTap: matchingEmotes.isNotEmpty ? () => widget.chatStore.addEmote(emote) : null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
|
|
@ -65,10 +96,10 @@ class RecentEmotesPanel extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: assetsStore.recentEmotes.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: widget.chatStore.assetsStore.recentEmotes.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
|
||||
|
|
@ -11,61 +13,105 @@ class ChatBottomBar extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
tooltip: 'Chat Details',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: ChatDetails(
|
||||
chatDetails: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
builder: (context) {
|
||||
final emotes = [
|
||||
...chatStore.assetsStore.userEmoteToObject.values,
|
||||
...chatStore.assetsStore.bttvEmotes,
|
||||
...chatStore.assetsStore.ffzEmotes,
|
||||
...chatStore.assetsStore.sevenTVEmotes
|
||||
].where((emote) => emote.name.toLowerCase().contains(chatStore.textController.text.split(' ').last.toLowerCase())).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (chatStore.settings.emoteAutocomplete && chatStore.showAutocomplete && emotes.isNotEmpty) ...[
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
child: TextField(
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Emote Menu',
|
||||
icon: const Icon(Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
itemCount: emotes.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index], autocompleteMode: true),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
isDense: true,
|
||||
hintMaxLines: 1,
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
hintText: chatStore.auth.isLoggedIn ? 'Send a message' : 'Log in to chat',
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
)
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
tooltip: 'Chat Details',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: ChatDetails(
|
||||
chatDetails: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: chatStore.channelName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
child: TextField(
|
||||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Emote Menu',
|
||||
icon: const Icon(Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
),
|
||||
hintMaxLines: 1,
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
hintText: chatStore.auth.isLoggedIn ? 'Send a message' : 'Log in to chat',
|
||||
),
|
||||
controller: chatStore.textController,
|
||||
onSubmitted: chatStore.sendMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ abstract class _ChatStoreBase with Store {
|
|||
@readonly
|
||||
var _autoScroll = true;
|
||||
|
||||
@readonly
|
||||
var _showAutocomplete = false;
|
||||
|
||||
/// The logged-in user's appearance in chat.
|
||||
@readonly
|
||||
var _userState = const USERSTATE();
|
||||
|
|
@ -129,7 +132,7 @@ abstract class _ChatStoreBase with Store {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a listener to the textfield that will do the following when focused:
|
||||
// Add a listener to the textfield focus that will do the following when focused:
|
||||
// 1. Hide the emote menu if it is currently shown
|
||||
// 2. Scroll to the latest message (after a brief delay)
|
||||
textFieldFocusNode.addListener(() {
|
||||
|
|
@ -142,6 +145,11 @@ abstract class _ChatStoreBase with Store {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Add a listener to the textfield that will show/hide the autocomplete bar if focused.
|
||||
// Will also rebuild the autocomplete bar when typing, refreshing the results as the user types.
|
||||
textController
|
||||
.addListener(() => _showAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty ? true : false);
|
||||
}
|
||||
|
||||
/// Handle and process the provided string-representation of the IRC data.
|
||||
|
|
@ -344,6 +352,28 @@ abstract class _ChatStoreBase with Store {
|
|||
textController.clear();
|
||||
}
|
||||
|
||||
/// Adds the given [emote] to the chat textfield.
|
||||
@action
|
||||
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
||||
if (textController.text.isEmpty || textController.text.endsWith(' ')) {
|
||||
textController.text += emote.name + ' ';
|
||||
} else if (autocompleteMode && _showAutocomplete && textController.text.endsWith('')) {
|
||||
final split = textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add(emote.name + ' ');
|
||||
|
||||
textController.text = split.join(' ');
|
||||
} else {
|
||||
textController.text += ' ' + emote.name + ' ';
|
||||
}
|
||||
|
||||
assetsStore.recentEmotes
|
||||
..removeWhere((recentEmote) => recentEmote.name == emote.name && recentEmote.type == emote.type)
|
||||
..insert(0, emote);
|
||||
|
||||
textController.selection = TextSelection.fromPosition(TextPosition(offset: textController.text.length));
|
||||
}
|
||||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_channel?.sink.close(1001);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,24 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$_showAutocompleteAtom =
|
||||
Atom(name: '_ChatStoreBase._showAutocomplete');
|
||||
|
||||
bool get showAutocomplete {
|
||||
_$_showAutocompleteAtom.reportRead();
|
||||
return super._showAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _showAutocomplete => showAutocomplete;
|
||||
|
||||
@override
|
||||
set _showAutocomplete(bool value) {
|
||||
_$_showAutocompleteAtom.reportWrite(value, super._showAutocomplete, () {
|
||||
super._showAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$_userStateAtom = Atom(name: '_ChatStoreBase._userState');
|
||||
|
||||
USERSTATE get userState {
|
||||
|
|
@ -107,6 +125,17 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
||||
final _$actionInfo = _$_ChatStoreBaseActionController.startAction(
|
||||
name: '_ChatStoreBase.addEmote');
|
||||
try {
|
||||
return super.addEmote(emote, autocompleteMode: autocompleteMode);
|
||||
} finally {
|
||||
_$_ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
|
|
@ -36,10 +36,24 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Bottom Bar'),
|
||||
subtitle: const Text('Displays the bottom text field bar.'),
|
||||
subtitle: const Text('Shows the bottom text field bar.'),
|
||||
value: settingsStore.showBottomBar,
|
||||
onChanged: (newValue) => settingsStore.showBottomBar = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Emote Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes when typing.'),
|
||||
value: settingsStore.emoteAutocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.emoteAutocomplete = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Zero-Width Emotes'),
|
||||
subtitle: const Text('Shows "stacked" emotes from BetterTTV and 7TV.'),
|
||||
value: settingsStore.showZeroWidth,
|
||||
onChanged: (newValue) => settingsStore.showZeroWidth = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Readable Name Colors'),
|
||||
|
|
@ -47,13 +61,6 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.useReadableColors,
|
||||
onChanged: (newValue) => settingsStore.useReadableColors = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Zero-Width Emotes'),
|
||||
subtitle: const Text('Displays "stacked" emotes from BetterTTV and 7TV.'),
|
||||
value: settingsStore.showZeroWidth,
|
||||
onChanged: (newValue) => settingsStore.showZeroWidth = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Show Deleted Messages'),
|
||||
|
|
@ -64,14 +71,14 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message Dividers'),
|
||||
subtitle: const Text('Displays a subtle divider between each message.'),
|
||||
subtitle: const Text('Shows a subtle divider between each message.'),
|
||||
value: settingsStore.showChatMessageDividers,
|
||||
onChanged: (newValue) => settingsStore.showChatMessageDividers = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message Timestamps'),
|
||||
subtitle: const Text('Displays timestamps for when a message was sent.'),
|
||||
subtitle: const Text('Shows timestamps for when a message was sent.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.timestampType,
|
||||
onChanged: (TimestampType? newTimestamp) => settingsStore.timestampType = newTimestamp!,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class GeneralSettings extends StatelessWidget {
|
|||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Stream Uptime on Thumbnails'),
|
||||
subtitle: const Text('Displays the uptime of the stream in the HH:MM:SS format.'),
|
||||
subtitle: const Text('Shows the uptime of the stream in the HH:MM:SS format.'),
|
||||
value: settingsStore.showThumbnailUptime,
|
||||
onChanged: settingsStore.showThumbnails ? (newValue) => settingsStore.showThumbnailUptime = newValue : null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class OtherSettings extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
|
|
@ -112,7 +112,7 @@ class OtherSettings extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.restore),
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
static const defaultEmoteScale = 1.0;
|
||||
|
||||
static const defaultEmoteAutocomplete = true;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
|
|
@ -123,6 +125,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var emoteScale = defaultEmoteScale;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteAutocomplete)
|
||||
@observable
|
||||
var emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
|
||||
// * Other settings
|
||||
static const defaultSendCrashLogs = true;
|
||||
|
||||
|
|
@ -168,6 +174,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
messageSpacing = defaultMessageSpacing;
|
||||
badgeScale = defaultBadgeScale;
|
||||
emoteScale = defaultEmoteScale;
|
||||
emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
|
||||
// * Other settings
|
||||
sendCrashLogs = defaultSendCrashLogs;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteAutocomplete = json['emoteAutocomplete'] as bool? ?? true
|
||||
..sendCrashLogs = json['sendCrashLogs'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
..expandInfo = json['expandInfo'] as bool? ?? true;
|
||||
|
|
@ -56,6 +57,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'messageScale': instance.messageScale,
|
||||
'badgeScale': instance.badgeScale,
|
||||
'emoteScale': instance.emoteScale,
|
||||
'emoteAutocomplete': instance.emoteAutocomplete,
|
||||
'sendCrashLogs': instance.sendCrashLogs,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'expandInfo': instance.expandInfo,
|
||||
|
|
@ -358,6 +360,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$emoteAutocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.emoteAutocomplete');
|
||||
|
||||
@override
|
||||
bool get emoteAutocomplete {
|
||||
_$emoteAutocompleteAtom.reportRead();
|
||||
return super.emoteAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteAutocomplete(bool value) {
|
||||
_$emoteAutocompleteAtom.reportWrite(value, super.emoteAutocomplete, () {
|
||||
super.emoteAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$sendCrashLogsAtom = Atom(name: '_SettingsStoreBase.sendCrashLogs');
|
||||
|
||||
@override
|
||||
|
|
@ -438,6 +456,7 @@ messageSpacing: ${messageSpacing},
|
|||
messageScale: ${messageScale},
|
||||
badgeScale: ${badgeScale},
|
||||
emoteScale: ${emoteScale},
|
||||
emoteAutocomplete: ${emoteAutocomplete},
|
||||
sendCrashLogs: ${sendCrashLogs},
|
||||
fullScreen: ${fullScreen},
|
||||
expandInfo: ${expandInfo}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue