mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-15 05:12:05 +10:00
feat: add contact UI helpers and path editor for routing management
- Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types. - Created colorForName and firstCharacterOrEmoji functions to enhance contact display. - Developed PathEditorSheet widget for managing contact paths with a user-friendly interface. - Introduced RoutingSheet for managing contact routing modes and displaying path history. - Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation.
This commit is contained in:
@@ -1,36 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class MessageStatusIcon extends StatelessWidget {
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class MessageStatusIcon extends StatefulWidget {
|
||||
final bool isAcked;
|
||||
final bool isFailed;
|
||||
final bool isPending;
|
||||
final bool isRepeated;
|
||||
final double size;
|
||||
|
||||
/// Base tint for the sent/sending state. On a colored (outgoing) bubble a
|
||||
/// plain grey tick is nearly invisible, so callers can pass the bubble's own
|
||||
/// meta/text color for contrast. Falls back to [ColorScheme.onSurfaceVariant].
|
||||
final Color? onColor;
|
||||
|
||||
const MessageStatusIcon({
|
||||
super.key,
|
||||
required this.isAcked,
|
||||
this.isFailed = false,
|
||||
this.isPending = false,
|
||||
this.isRepeated = false,
|
||||
this.size = 14,
|
||||
this.onColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageStatusIcon> createState() => _MessageStatusIconState();
|
||||
}
|
||||
|
||||
class _MessageStatusIconState extends State<MessageStatusIcon>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
);
|
||||
if (widget.isPending) _controller.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageStatusIcon oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isPending && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
} else if (!widget.isPending && _controller.isAnimating) {
|
||||
_controller
|
||||
..stop()
|
||||
..reset();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final double size = widget.size;
|
||||
final Color baseColor = widget.onColor ?? colorScheme.onSurfaceVariant;
|
||||
|
||||
if (widget.isFailed) {
|
||||
return Semantics(
|
||||
label: l10n.messageStatus_failed,
|
||||
child: Icon(Icons.cancel, size: size, color: colorScheme.error),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isPending) {
|
||||
return Semantics(
|
||||
label: l10n.messageStatus_pending,
|
||||
child: _SendingDots(
|
||||
controller: _controller,
|
||||
color: baseColor,
|
||||
size: size,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final bool delivered = widget.isAcked || widget.isRepeated;
|
||||
final String label = widget.isRepeated
|
||||
? l10n.messageStatus_repeated
|
||||
: widget.isAcked
|
||||
? l10n.messageStatus_delivered
|
||||
: l10n.messageStatus_sent;
|
||||
final Color color = delivered ? colorScheme.tertiary : baseColor;
|
||||
|
||||
return Semantics(
|
||||
label: label,
|
||||
child: delivered
|
||||
? SvgPicture.asset(
|
||||
'assets/icons/done_all.svg',
|
||||
width: size,
|
||||
height: size,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
)
|
||||
: Icon(Icons.done, size: size, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Three dots that pulse left-to-right while a message is in flight.
|
||||
class _SendingDots extends StatelessWidget {
|
||||
final AnimationController controller;
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
const _SendingDots({
|
||||
required this.controller,
|
||||
required this.color,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFailed) {
|
||||
return Icon(Icons.cancel, size: size, color: Colors.red);
|
||||
}
|
||||
|
||||
final Color color;
|
||||
if (isAcked) {
|
||||
color = Colors.green;
|
||||
} else {
|
||||
color = Colors.grey;
|
||||
}
|
||||
|
||||
return SvgPicture.asset(
|
||||
'assets/icons/done_all.svg',
|
||||
width: size,
|
||||
final double dot = (size * 0.24).clamp(2.0, 4.0);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: List.generate(3, (i) {
|
||||
final double phase = (controller.value - i * 0.18) % 1.0;
|
||||
final double t = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
|
||||
final double opacity = 0.25 + 0.75 * t.clamp(0.0, 1.0);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: dot * 0.28),
|
||||
child: Container(
|
||||
width: dot,
|
||||
height: dot,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: opacity),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user