189 lines
5.3 KiB
Dart
189 lines
5.3 KiB
Dart
import "dart:typed_data";
|
|
import "package:clawd_code/ui/widgets/common/button.dart";
|
|
import "package:shadcn_flutter/shadcn_flutter.dart";
|
|
import "package:flutter/services.dart";
|
|
import "../../../models/attachment.dart";
|
|
import "../attachment_preview.dart";
|
|
import "../../../../src/session/session_types.dart";
|
|
|
|
Color _msgColour(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final dark = theme.brightness == Brightness.dark;
|
|
final h = HSLColor.fromColor(theme.colorScheme.border).hue;
|
|
return dark
|
|
? HSLColor.fromAHSL(1, h, 0.25, 0.25).toColor()
|
|
: HSLColor.fromAHSL(1, h, 0.30, 0.6).toColor();
|
|
}
|
|
|
|
class UserBubble extends StatefulWidget {
|
|
const UserBubble({
|
|
super.key,
|
|
required this.content,
|
|
this.attachments,
|
|
this.timestamp,
|
|
this.onRetry,
|
|
this.onEdit,
|
|
});
|
|
|
|
final String content;
|
|
final List<MessageAttachment>? attachments;
|
|
final DateTime? timestamp;
|
|
final VoidCallback? onRetry;
|
|
final VoidCallback? onEdit;
|
|
|
|
@override
|
|
State<UserBubble> createState() => _UserBubbleState();
|
|
}
|
|
|
|
class _UserBubbleState extends State<UserBubble> {
|
|
bool _hovered = false;
|
|
|
|
String _formatTime(DateTime dt) {
|
|
final local = dt.toLocal();
|
|
final now = DateTime.now();
|
|
final h = local.hour.toString().padLeft(2, "0");
|
|
final m = local.minute.toString().padLeft(2, "0");
|
|
final time = "$h:$m";
|
|
|
|
final isToday = local.year == now.year && local.month == now.month && local.day == now.day;
|
|
if (isToday) return "Today at $time";
|
|
|
|
// show date for anything older
|
|
final day = local.day.toString().padLeft(2, "0");
|
|
final month = local.month.toString().padLeft(2, "0");
|
|
return "${local.year == now.year ? "" : "${local.year}/"}$month/$day at $time";
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final atts = widget.attachments;
|
|
final muted = Theme.of(context).colorScheme.mutedForeground;
|
|
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
|
|
if (atts != null && atts.isNotEmpty) ...[
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
reverse: true,
|
|
child: Row(
|
|
children: [
|
|
for (final att in atts)
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8),
|
|
child: AttachmentItem(
|
|
attachment: Attachment(
|
|
name: att.name,
|
|
mimeType: att.mimeType,
|
|
data: Uint8List.fromList(att.data),
|
|
),
|
|
onRemove: () {},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Gap(6),
|
|
],
|
|
|
|
OutlinedContainer(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
borderColor: _msgColour(context).scaleAlpha(0.9),
|
|
backgroundColor: _msgColour(context),
|
|
boxShadow: [],
|
|
child: SelectableText(widget.content),
|
|
),
|
|
|
|
const Gap(6),
|
|
|
|
AnimatedOpacity(
|
|
opacity: _hovered ? 1.0 : 0.0,
|
|
duration: const Duration(milliseconds: 150),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
|
|
if (widget.timestamp != null)
|
|
Text(
|
|
_formatTime(widget.timestamp!),
|
|
style: TextStyle(color: muted),
|
|
).xSmall,
|
|
|
|
const Gap(10),
|
|
|
|
_ActionBtn(
|
|
icon: LucideIcons.refreshCw,
|
|
tooltip: "Retry",
|
|
onTap: widget.onRetry,
|
|
color: muted,
|
|
),
|
|
_ActionBtn(
|
|
icon: LucideIcons.pencil,
|
|
tooltip: "Edit",
|
|
onTap: widget.onEdit,
|
|
color: muted,
|
|
),
|
|
_ActionBtn(
|
|
icon: LucideIcons.copy,
|
|
tooltip: "Copy",
|
|
color: muted,
|
|
onTap: () {
|
|
Clipboard.setData(ClipboardData(text: widget.content));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
],
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
|
|
class _ActionBtn extends StatelessWidget {
|
|
const _ActionBtn({
|
|
required this.icon,
|
|
required this.tooltip,
|
|
required this.color,
|
|
this.onTap,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String tooltip;
|
|
final Color color;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
if (true) {
|
|
return IconButton.ghost(
|
|
onPressed: () {
|
|
if (onTap != null) onTap!();
|
|
},
|
|
icon: Icon(
|
|
icon,
|
|
).iconSmall.iconMutedForeground,
|
|
);
|
|
}
|
|
|
|
return Tooltip(
|
|
tooltip: TooltipContainer(child: Text(tooltip)),
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
|
child: Icon(icon, size: 14, color: color),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|