refactor: Gott-Datei message_group.dart in 7 Dateien aufgeteilt
Gleiche Technik wie bei settings_modal.dart: reine Verschiebung per Dart part/part-of, keine Logikänderung. message_group.dart schrumpft von 2775 auf 30 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze (Baseline unverändert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
part of '../message_group.dart';
|
||||
|
||||
class _HoverActions extends StatefulWidget {
|
||||
final PyramidTheme pt;
|
||||
final Event event;
|
||||
final List<Event> events;
|
||||
final Room? room;
|
||||
final String currentUserId;
|
||||
final VoidCallback? onReply;
|
||||
final ValueChanged<bool>? onOverlayStateChanged;
|
||||
|
||||
const _HoverActions({
|
||||
required this.pt,
|
||||
required this.event,
|
||||
required this.events,
|
||||
required this.currentUserId,
|
||||
this.room,
|
||||
this.onReply,
|
||||
this.onOverlayStateChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_HoverActions> createState() => _HoverActionsState();
|
||||
}
|
||||
|
||||
class _HoverActionsState extends State<_HoverActions> {
|
||||
OverlayEntry? _reactionOverlay;
|
||||
List<String> _quickReactions = ReactionService.quickReactions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ReactionService.load().then((_) {
|
||||
if (mounted) setState(() => _quickReactions = ReactionService.quickReactions);
|
||||
});
|
||||
}
|
||||
|
||||
void _closeReactionOverlay() {
|
||||
_reactionOverlay?.remove();
|
||||
_reactionOverlay = null;
|
||||
widget.onOverlayStateChanged?.call(false);
|
||||
}
|
||||
|
||||
void _toggleReactionPicker() {
|
||||
if (_reactionOverlay != null) {
|
||||
_closeReactionOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
final offset = box?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||
|
||||
_reactionOverlay = OverlayEntry(builder: (ctx) {
|
||||
final screenSize = MediaQuery.sizeOf(ctx);
|
||||
const pickerW = 340.0;
|
||||
const pickerH = 380.0;
|
||||
double left = (offset.dx - pickerW / 2).clamp(8.0, screenSize.width - pickerW - 8);
|
||||
double top = (offset.dy - pickerH - 8).clamp(8.0, screenSize.height - pickerH - 8);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(children: [
|
||||
GestureDetector(
|
||||
onTap: _closeReactionOverlay,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (emoji) {
|
||||
_closeReactionOverlay();
|
||||
widget.room?.sendReaction(widget.event.eventId, emoji)
|
||||
.catchError((_) => '');
|
||||
ReactionService.recordUsed(emoji).then((_) {
|
||||
if (mounted) setState(() => _quickReactions = ReactionService.quickReactions);
|
||||
});
|
||||
},
|
||||
onClose: _closeReactionOverlay,
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
widget.onOverlayStateChanged?.call(true);
|
||||
Overlay.of(context).insert(_reactionOverlay!);
|
||||
}
|
||||
|
||||
void _showMoreMenu() {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
final offset = box?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||
final size = box?.size ?? Size.zero;
|
||||
|
||||
final isOwnMessage = widget.event.senderId == widget.currentUserId;
|
||||
final text = widget.event.body;
|
||||
|
||||
// Find any image or sticker event in the group that can be saved as a sticker.
|
||||
final imageEvent = widget.events.firstWhereOrNull(
|
||||
(e) => e.messageType == 'm.image' || e.type == EventTypes.Sticker,
|
||||
);
|
||||
|
||||
// Capture messenger synchronously before showMenu's async gap so the
|
||||
// snackbar can be shown even if the widget is unmounted by then.
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
|
||||
showMenu<String>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
offset.dx, offset.dy - size.height,
|
||||
offset.dx + size.width, offset.dy,
|
||||
),
|
||||
items: [
|
||||
PopupMenuItem(value: 'copy', child: Row(children: [
|
||||
Icon(Icons.copy_rounded, size: 16, color: widget.pt.fgMuted),
|
||||
const SizedBox(width: 10),
|
||||
Text('Kopieren', style: TextStyle(color: widget.pt.fg, fontSize: 14)),
|
||||
])),
|
||||
if (imageEvent != null)
|
||||
PopupMenuItem(value: 'save_sticker', child: Row(children: [
|
||||
Icon(Icons.sticky_note_2_outlined, size: 16, color: widget.pt.fgMuted),
|
||||
const SizedBox(width: 10),
|
||||
Text('Als Sticker speichern', style: TextStyle(color: widget.pt.fg, fontSize: 14)),
|
||||
])),
|
||||
if (isOwnMessage)
|
||||
PopupMenuItem(value: 'delete', child: Row(children: [
|
||||
Icon(Icons.delete_outline_rounded, size: 16, color: widget.pt.danger),
|
||||
const SizedBox(width: 10),
|
||||
Text('Löschen', style: TextStyle(color: widget.pt.danger, fontSize: 14)),
|
||||
])),
|
||||
PopupMenuItem(value: 'pin', child: Row(children: [
|
||||
Icon(Icons.push_pin_outlined, size: 16, color: widget.pt.fgMuted),
|
||||
const SizedBox(width: 10),
|
||||
Text('Anpinnen', style: TextStyle(color: widget.pt.fg, fontSize: 14)),
|
||||
])),
|
||||
],
|
||||
).then((value) async {
|
||||
if (value == 'copy') {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
} else if (value == 'save_sticker' && imageEvent != null) {
|
||||
final ok = await GifFavoriteService.addImageAsStickerFavorite(imageEvent);
|
||||
messenger?.showSnackBar(SnackBar(
|
||||
content: Text(ok ? 'Als Sticker gespeichert' : 'Fehler beim Speichern'),
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
} else if (value == 'delete') {
|
||||
widget.event.redactEvent(reason: 'Vom Nutzer gelöscht')
|
||||
.catchError((_) => null);
|
||||
} else if (value == 'pin') {
|
||||
final room = widget.room;
|
||||
if (room == null) return;
|
||||
final pinned = List<String>.from(
|
||||
room.getState(EventTypes.RoomPinnedEvents)?.content['pinned'] as List? ?? [],
|
||||
);
|
||||
final eventId = widget.event.eventId;
|
||||
if (!pinned.contains(eventId)) pinned.add(eventId);
|
||||
room.setPinnedEvents(pinned).catchError((_) => '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_reactionOverlay != null) {
|
||||
_reactionOverlay!.remove();
|
||||
_reactionOverlay = null;
|
||||
widget.onOverlayStateChanged?.call(false);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = widget.pt;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: pt.bg0,
|
||||
borderRadius: BorderRadius.circular(pt.rBase),
|
||||
border: Border.all(color: pt.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(100),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Quick reactions
|
||||
..._quickReactions.map((emoji) => _QuickReactionBtn(
|
||||
emoji: emoji,
|
||||
pt: pt,
|
||||
onTap: () {
|
||||
widget.room?.sendReaction(widget.event.eventId, emoji)
|
||||
.catchError((_) => '');
|
||||
ReactionService.recordUsed(emoji).then((_) {
|
||||
if (mounted) setState(() => _quickReactions = ReactionService.quickReactions);
|
||||
});
|
||||
},
|
||||
)),
|
||||
Container(width: 1, height: 18, color: pt.border, margin: const EdgeInsets.symmetric(horizontal: 2)),
|
||||
_ActionBtn(
|
||||
icon: Icons.add_reaction_outlined,
|
||||
tooltip: 'Reaktion',
|
||||
pt: pt,
|
||||
accent: true,
|
||||
onTap: _toggleReactionPicker,
|
||||
),
|
||||
_ActionBtn(
|
||||
icon: Icons.reply_rounded,
|
||||
tooltip: 'Antworten',
|
||||
pt: pt,
|
||||
onTap: widget.onReply,
|
||||
),
|
||||
_ActionBtn(
|
||||
icon: Icons.more_horiz_rounded,
|
||||
tooltip: 'Mehr',
|
||||
pt: pt,
|
||||
onTap: _showMoreMenu,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionBtn extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String tooltip;
|
||||
final PyramidTheme pt;
|
||||
final bool accent;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ActionBtn({
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.pt,
|
||||
this.accent = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ActionBtn> createState() => _ActionBtnState();
|
||||
}
|
||||
|
||||
class _ActionBtnState extends State<_ActionBtn> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = _hovered
|
||||
? (widget.accent ? widget.pt.accent : widget.pt.fg)
|
||||
: widget.pt.fgMuted;
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Tooltip(
|
||||
message: widget.tooltip,
|
||||
waitDuration: const Duration(milliseconds: 400),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: _hovered ? widget.pt.bgHover : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(widget.pt.rSm),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(widget.icon, size: 16, color: color),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user