move clear recent emotes action to tab bar icon button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-03-03 20:40:33 -08:00
parent 93c7b5b637
commit d500119219
3 changed files with 168 additions and 124 deletions

View file

@ -6,6 +6,7 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
import 'package:frosty/utils/context_extensions.dart';
import 'package:frosty/widgets/frosty_dialog.dart';
import 'package:frosty/widgets/frosty_page_view.dart';
import 'package:frosty/widgets/frosty_scrollbar.dart';
@ -192,6 +193,47 @@ class Chat extends StatelessWidget {
if (chatStore.settings.showBTTVEmotes) 'BTTV',
if (chatStore.settings.showFFZEmotes) 'FFZ',
],
tabActions: {
if (chatStore
.assetsStore
.recentEmotes
.isNotEmpty)
0: IconButton(
onPressed: () => showDialog(
context: context,
builder: (context) => FrostyDialog(
title: 'Clear recent emotes',
message:
'Are you sure you want to clear your recent emotes?',
actions: [
TextButton(
onPressed:
Navigator.of(context).pop,
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
chatStore
.assetsStore
.recentEmotes
.clear();
chatStore.updateNotification(
'Recent emotes cleared',
);
Navigator.pop(context);
},
child: const Text('Clear'),
),
],
),
),
icon: const Icon(
Icons.delete_outline_rounded,
),
tooltip: 'Clear recent emotes',
iconSize: 20,
),
},
children: [
RecentEmotesPanel(chatStore: chatStore),
if (chatStore.settings.showTwitchEmotes)

View file

@ -8,7 +8,6 @@ import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/utils/context_extensions.dart';
import 'package:frosty/widgets/alert_message.dart';
import 'package:frosty/widgets/frosty_cached_network_image.dart';
import 'package:frosty/widgets/frosty_dialog.dart';
import 'package:provider/provider.dart';
class RecentEmotesPanel extends StatelessWidget {
@ -16,122 +15,74 @@ class RecentEmotesPanel extends StatelessWidget {
const RecentEmotesPanel({super.key, required this.chatStore});
Future<void> _showClearDialog(BuildContext context) {
return showDialog(
context: context,
builder: (context) => FrostyDialog(
title: 'Clear recent emotes',
message: 'Are you sure you want to clear your recent emotes?',
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
chatStore.assetsStore.recentEmotes.clear();
chatStore.updateNotification('Recent emotes cleared');
Navigator.pop(context);
},
child: const Text('Clear'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Observer(
builder: (context) {
if (chatStore.assetsStore.recentEmotes.isEmpty) {
return const Center(
child: AlertMessage(message: 'No recent emotes'),
);
}
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: CustomScrollView(
slivers: [
if (chatStore.assetsStore.recentEmotes.isEmpty)
const SliverFillRemaining(
hasScrollBody: false,
child: AlertMessage(message: 'No recent emotes'),
)
else
SliverGrid.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: context.isPortrait
? 8
: context.read<SettingsStore>().showVideo
? 6
: 16,
),
itemBuilder: (context, index) {
final emote = chatStore.assetsStore.recentEmotes[index];
final validEmotes = [
...chatStore.assetsStore.emoteToObject.values,
...chatStore.assetsStore.userEmoteToObject.values,
];
final matchingEmotes = validEmotes.where(
(existingEmote) =>
existingEmote.name == emote.name &&
existingEmote.type == emote.type,
);
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: context.isPortrait
? 8
: context.read<SettingsStore>().showVideo
? 6
: 16,
),
itemBuilder: (context, index) {
final emote = chatStore.assetsStore.recentEmotes[index];
final validEmotes = [
...chatStore.assetsStore.emoteToObject.values,
...chatStore.assetsStore.userEmoteToObject.values,
];
final matchingEmotes = validEmotes.where(
(existingEmote) =>
existingEmote.name == emote.name &&
existingEmote.type == emote.type,
);
return InkWell(
onTap: matchingEmotes.isNotEmpty
? () => chatStore.addEmote(emote)
: null,
onLongPress: () {
HapticFeedback.lightImpact();
return InkWell(
onTap: matchingEmotes.isNotEmpty
? () => chatStore.addEmote(emote)
: null,
onLongPress: () {
HapticFeedback.lightImpact();
IRCMessage.showEmoteDetailsBottomSheet(
context,
emote: emote,
launchExternal: chatStore.settings.launchUrlExternal,
);
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: FrostyCachedNetworkImage(
imageUrl: matchingEmotes.isNotEmpty
? matchingEmotes.first.url
: emote.url,
color: matchingEmotes.isNotEmpty
? null
: const Color.fromRGBO(255, 255, 255, 0.5),
colorBlendMode: matchingEmotes.isNotEmpty
? null
: BlendMode.modulate,
height:
emote.height?.toDouble() ?? defaultEmoteSize,
width: emote.width?.toDouble(),
),
),
),
);
},
itemCount: chatStore.assetsStore.recentEmotes.length,
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
width: double.infinity,
child: TextButton.icon(
onPressed: () => _showClearDialog(context),
icon: const Icon(Icons.clear_all_rounded),
label: const Text('Clear recent emotes'),
),
),
IRCMessage.showEmoteDetailsBottomSheet(
context,
emote: emote,
launchExternal: chatStore.settings.launchUrlExternal,
);
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: FrostyCachedNetworkImage(
imageUrl: matchingEmotes.isNotEmpty
? matchingEmotes.first.url
: emote.url,
color: matchingEmotes.isNotEmpty
? null
: const Color.fromRGBO(255, 255, 255, 0.5),
colorBlendMode: matchingEmotes.isNotEmpty
? null
: BlendMode.modulate,
height:
emote.height?.toDouble() ?? defaultEmoteSize,
width: emote.width?.toDouble(),
),
),
),
),
],
);
},
itemCount: chatStore.assetsStore.recentEmotes.length,
),
);
},

View file

@ -4,17 +4,55 @@ class FrostyPageView extends StatefulWidget {
final List<String> headers;
final List<Widget> children;
/// Actions keyed by tab index, shown trailing the tab bar
/// when that tab is selected.
final Map<int, Widget> tabActions;
const FrostyPageView({
super.key,
required this.headers,
required this.children,
this.tabActions = const {},
}) : assert(headers.length == children.length);
@override
State<FrostyPageView> createState() => _FrostyPageViewState();
}
class _FrostyPageViewState extends State<FrostyPageView> {
class _FrostyPageViewState extends State<FrostyPageView>
with TickerProviderStateMixin {
late TabController _tabController;
void _handleTabChange() => setState(() {});
void _initTabController(int length) {
_tabController = TabController(length: length, vsync: this);
_tabController.addListener(_handleTabChange);
}
@override
void initState() {
super.initState();
_initTabController(widget.headers.length);
}
@override
void didUpdateWidget(FrostyPageView oldWidget) {
super.didUpdateWidget(oldWidget);
if (_tabController.length != widget.headers.length) {
_tabController.removeListener(_handleTabChange);
_tabController.dispose();
_initTabController(widget.headers.length);
}
}
@override
void dispose() {
_tabController.removeListener(_handleTabChange);
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
@ -26,26 +64,39 @@ class _FrostyPageViewState extends State<FrostyPageView> {
return const SizedBox.expand();
}
return DefaultTabController(
length: widget.headers.length,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
isScrollable: true,
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
final trailingAction = widget.tabActions[_tabController.index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TabBar(
controller: _tabController,
isScrollable: true,
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
labelPadding:
const EdgeInsets.symmetric(horizontal: 12),
tabs: widget.headers
.map((header) => Tab(text: header))
.toList(),
),
),
labelPadding: const EdgeInsets.symmetric(horizontal: 12),
tabs: widget.headers
.map((header) => Tab(text: header))
.toList(),
?trailingAction,
],
),
const Divider(),
Expanded(
child: TabBarView(
controller: _tabController,
children: widget.children,
),
const Divider(),
Expanded(child: TabBarView(children: widget.children)),
],
),
),
],
);
},
);