mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
refactor(mobile): route avatar shapes through one bottleneck
avatar_shape.dart now owns the agent squircle and the person circle. avatarShape(isAgent:), avatarPath() and AvatarClip are the only way to get either, and the squircle border is private. A source guard test fails when code keys an avatar shape on agent identity inline, or uses a person shape on an agent-only surface. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@squareup.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import '../../shared/emoji/emoji_avatar.dart';
|
||||
import '../../shared/emoji/emoji_data_provider.dart';
|
||||
import '../../shared/relay/relay.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/widgets/avatar_image_source_controls.dart';
|
||||
import '../../shared/widgets/image_avatar_capture.dart';
|
||||
@@ -229,8 +229,8 @@ class AgentAvatarPicker extends HookConsumerWidget {
|
||||
child: SizedBox.square(
|
||||
dimension: 220,
|
||||
child: selection.bytes != null
|
||||
? ClipPath(
|
||||
clipper: const ShapeBorderClipper(shape: AgentAvatarBorder()),
|
||||
? AvatarClip(
|
||||
isAgent: true,
|
||||
child: Image.memory(selection.bytes!, fit: BoxFit.cover),
|
||||
)
|
||||
: PresetAgentAvatar(
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../shared/animated_avatar.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/widgets/progressive_animated_avatar.dart';
|
||||
|
||||
@@ -117,10 +117,8 @@ class AgentCard extends StatelessWidget {
|
||||
child: RepaintBoundary(
|
||||
child: SizedBox.square(
|
||||
dimension: 104,
|
||||
child: ClipPath(
|
||||
clipper: const ShapeBorderClipper(
|
||||
shape: AgentAvatarBorder(),
|
||||
),
|
||||
child: AvatarClip(
|
||||
isAgent: true,
|
||||
child: portrait,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'agent_avatar_picker.dart';
|
||||
import '../../shared/community/community.dart';
|
||||
import '../../shared/community/community_provider.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/frosted_app_bar.dart';
|
||||
import '../../shared/widgets/frosted_scaffold.dart';
|
||||
import '../../shared/widgets/ios_glass_navigation_action.dart';
|
||||
@@ -198,10 +198,8 @@ class CreateAgentPage extends HookConsumerWidget {
|
||||
child: SizedBox.square(
|
||||
dimension: 160,
|
||||
child: avatar.value.bytes != null
|
||||
? ClipPath(
|
||||
clipper: const ShapeBorderClipper(
|
||||
shape: AgentAvatarBorder(),
|
||||
),
|
||||
? AvatarClip(
|
||||
isAgent: true,
|
||||
child: Image.memory(
|
||||
avatar.value.bytes!,
|
||||
fit: BoxFit.cover,
|
||||
|
||||
@@ -32,7 +32,7 @@ import '../../shared/identity_names/identity_names_provider.dart';
|
||||
import '../../shared/mentions/agent_identity_provider.dart';
|
||||
import '../../shared/relay/relay.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/widgets/buzz_loading_indicator.dart';
|
||||
import '../../shared/widgets/bouncing_dots_indicator.dart';
|
||||
|
||||
@@ -189,9 +189,7 @@ class _HuddleCallAvatar extends HookConsumerWidget {
|
||||
width: speakingRingSize,
|
||||
height: speakingRingSize,
|
||||
decoration: ShapeDecoration(
|
||||
shape: isAgent
|
||||
? const AgentAvatarBorder()
|
||||
: const CircleBorder(),
|
||||
shape: avatarShape(isAgent: isAgent),
|
||||
color: context.colors.primary.withValues(
|
||||
alpha: 0.07,
|
||||
),
|
||||
@@ -215,9 +213,7 @@ class _HuddleCallAvatar extends HookConsumerWidget {
|
||||
width: avatarRadius * 2,
|
||||
height: avatarRadius * 2,
|
||||
decoration: ShapeDecoration(
|
||||
shape: isAgent
|
||||
? const AgentAvatarBorder()
|
||||
: const CircleBorder(),
|
||||
shape: avatarShape(isAgent: isAgent),
|
||||
color: context.controlSurface,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
|
||||
@@ -199,9 +199,7 @@ class _HuddleParticipantSpotlight extends ConsumerWidget {
|
||||
: const Duration(milliseconds: 180),
|
||||
padding: EdgeInsets.all(active ? Grid.xxs : Grid.half),
|
||||
decoration: ShapeDecoration(
|
||||
shape: isAgent
|
||||
? const AgentAvatarBorder()
|
||||
: const CircleBorder(),
|
||||
shape: avatarShape(isAgent: isAgent),
|
||||
color: context.colors.primary.withValues(
|
||||
alpha: active ? 0.18 : 0.08,
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/profile/user_profile.dart';
|
||||
|
||||
@@ -36,9 +36,7 @@ class SmallAvatar extends StatelessWidget {
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: ShapeDecoration(
|
||||
shape: isAgent
|
||||
? AgentAvatarBorder(side: side)
|
||||
: CircleBorder(side: side),
|
||||
shape: avatarShape(isAgent: isAgent, side: side),
|
||||
),
|
||||
child: AvatarImage(
|
||||
imageUrl: avatarUrl,
|
||||
|
||||
@@ -14,7 +14,7 @@ import '../../shared/identity_names/identity_names_provider.dart';
|
||||
import '../../shared/relay/relay.dart';
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/utils/string_utils.dart';
|
||||
import '../../shared/widgets/agent_avatar_border.dart';
|
||||
import '../../shared/widgets/avatar_shape.dart';
|
||||
import '../../shared/widgets/avatar_image.dart';
|
||||
import '../../shared/widgets/agent_artwork_panel.dart';
|
||||
import '../../shared/widgets/buzz_sheet_header.dart';
|
||||
@@ -472,10 +472,9 @@ class _ProfileAvatar extends HookWidget {
|
||||
imageUrl: animatedAvatar?.posterUrl ?? avatarUrl,
|
||||
fallback: _AvatarFallback(initial: initial),
|
||||
);
|
||||
if (!isAgent) return ClipOval(child: avatar);
|
||||
return ClipPath(
|
||||
clipper: const ShapeBorderClipper(shape: AgentAvatarBorder()),
|
||||
child: !usesInsetAgentArtwork(avatarUrl)
|
||||
return AvatarClip(
|
||||
isAgent: isAgent,
|
||||
child: !isAgent || !usesInsetAgentArtwork(avatarUrl)
|
||||
? avatar
|
||||
: Transform.scale(
|
||||
scale: animatedAvatarArtworkScale,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
/// Returns the agent avatar squircle scaled to [rect].
|
||||
///
|
||||
/// Matches desktop's `avatar-squircle.svg` mask so agent avatars share one
|
||||
/// silhouette across clients.
|
||||
Path agentAvatarPath(Rect rect) {
|
||||
Offset at(double x, double y) =>
|
||||
Offset(rect.left + rect.width * x, rect.top + rect.height * y);
|
||||
final start = at(.5, 0);
|
||||
final path = Path()..moveTo(start.dx, start.dy);
|
||||
void curve(Offset a, Offset b, Offset c) =>
|
||||
path.cubicTo(a.dx, a.dy, b.dx, b.dy, c.dx, c.dy);
|
||||
curve(at(.93, 0), at(1, .07), at(1, .5));
|
||||
curve(at(1, .93), at(.93, 1), at(.5, 1));
|
||||
curve(at(.07, 1), at(0, .93), at(0, .5));
|
||||
curve(at(0, .07), at(.07, 0), start);
|
||||
return path..close();
|
||||
}
|
||||
|
||||
/// The shape of every agent avatar, its halos, and its placeholders.
|
||||
class AgentAvatarBorder extends OutlinedBorder {
|
||||
const AgentAvatarBorder({super.side});
|
||||
|
||||
@override
|
||||
AgentAvatarBorder copyWith({BorderSide? side}) =>
|
||||
AgentAvatarBorder(side: side ?? this.side);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.strokeInset);
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) =>
|
||||
agentAvatarPath(rect.deflate(side.strokeInset));
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) =>
|
||||
agentAvatarPath(rect);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
if (side.style == BorderStyle.none) return;
|
||||
canvas.drawPath(
|
||||
agentAvatarPath(rect.inflate(side.strokeOffset / 2)),
|
||||
side.toPaint(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) => AgentAvatarBorder(side: side.scale(t));
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is AgentAvatarBorder && other.side == side;
|
||||
|
||||
@override
|
||||
int get hashCode => side.hashCode;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import '../animated_avatar.dart';
|
||||
import '../emoji/emoji_avatar.dart';
|
||||
import '../emoji/native_emoji_glyph.dart';
|
||||
import '../relay/relay.dart';
|
||||
import 'agent_avatar_border.dart';
|
||||
import 'avatar_shape.dart';
|
||||
|
||||
part 'avatar_image_native.dart';
|
||||
|
||||
@@ -47,26 +47,26 @@ class AvatarImage extends StatelessWidget {
|
||||
fallback: fallback,
|
||||
),
|
||||
);
|
||||
final clipped = AvatarClip(
|
||||
isAgent: isAgent,
|
||||
child: isAgent && usesInsetAgentArtwork(imageUrl)
|
||||
? Transform.scale(scale: animatedAvatarArtworkScale, child: content)
|
||||
: content,
|
||||
);
|
||||
// CircleAvatar keeps its theme default when people have no background.
|
||||
if (!isAgent) {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: color,
|
||||
child: ClipOval(child: content),
|
||||
child: clipped,
|
||||
);
|
||||
}
|
||||
|
||||
const shape = AgentAvatarBorder();
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(color: color, shape: shape),
|
||||
child: ClipPath(
|
||||
clipper: const ShapeBorderClipper(shape: shape),
|
||||
child: !usesInsetAgentArtwork(imageUrl)
|
||||
? content
|
||||
: Transform.scale(
|
||||
scale: animatedAvatarArtworkScale,
|
||||
child: content,
|
||||
),
|
||||
decoration: ShapeDecoration(
|
||||
color: color,
|
||||
shape: avatarShape(isAgent: true),
|
||||
),
|
||||
child: clipped,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,7 @@ Future<Uint8List?> nativeAvatarImage({
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
final bounds = const Rect.fromLTWH(0, 0, dimension, dimension);
|
||||
if (isAgent && !circular) {
|
||||
canvas.clipPath(agentAvatarPath(bounds));
|
||||
} else {
|
||||
canvas.clipRRect(
|
||||
RRect.fromRectAndRadius(bounds, const Radius.circular(dimension / 2)),
|
||||
);
|
||||
}
|
||||
canvas.clipPath(avatarPath(bounds, isAgent: isAgent && !circular));
|
||||
canvas.drawColor(background, BlendMode.src);
|
||||
final imageBounds = isAgent && usesInsetAgentArtwork(url)
|
||||
? bounds.deflate(dimension * (1 - animatedAvatarArtworkScale) / 2)
|
||||
|
||||
86
mobile/lib/shared/widgets/avatar_shape.dart
Normal file
86
mobile/lib/shared/widgets/avatar_shape.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// The single source of avatar silhouettes.
|
||||
///
|
||||
/// Agents use desktop's squircle (`avatar-squircle.svg`) and people use
|
||||
/// circles. Every avatar frame, clip, halo and placeholder takes its shape
|
||||
/// from here, so the two never drift apart across surfaces or clients.
|
||||
OutlinedBorder avatarShape({
|
||||
required bool isAgent,
|
||||
BorderSide side = BorderSide.none,
|
||||
}) => isAgent ? _AgentSquircleBorder(side: side) : CircleBorder(side: side);
|
||||
|
||||
/// The avatar silhouette scaled to [rect], for canvas painting.
|
||||
Path avatarPath(Rect rect, {required bool isAgent}) =>
|
||||
avatarShape(isAgent: isAgent).getOuterPath(rect);
|
||||
|
||||
/// Clips [child] to the avatar silhouette for [isAgent].
|
||||
class AvatarClip extends StatelessWidget {
|
||||
/// Creates an avatar clip.
|
||||
const AvatarClip({required this.isAgent, required this.child, super.key});
|
||||
|
||||
/// Whether [child] is an agent avatar.
|
||||
final bool isAgent;
|
||||
|
||||
/// The avatar artwork.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ClipPath(
|
||||
clipper: ShapeBorderClipper(shape: avatarShape(isAgent: isAgent)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Path _agentSquirclePath(Rect rect) {
|
||||
Offset at(double x, double y) =>
|
||||
Offset(rect.left + rect.width * x, rect.top + rect.height * y);
|
||||
final start = at(.5, 0);
|
||||
final path = Path()..moveTo(start.dx, start.dy);
|
||||
void curve(Offset a, Offset b, Offset c) =>
|
||||
path.cubicTo(a.dx, a.dy, b.dx, b.dy, c.dx, c.dy);
|
||||
curve(at(.93, 0), at(1, .07), at(1, .5));
|
||||
curve(at(1, .93), at(.93, 1), at(.5, 1));
|
||||
curve(at(.07, 1), at(0, .93), at(0, .5));
|
||||
curve(at(0, .07), at(.07, 0), start);
|
||||
return path..close();
|
||||
}
|
||||
|
||||
/// Private so agent avatars can only get their shape from [avatarShape].
|
||||
class _AgentSquircleBorder extends OutlinedBorder {
|
||||
const _AgentSquircleBorder({super.side});
|
||||
|
||||
@override
|
||||
_AgentSquircleBorder copyWith({BorderSide? side}) =>
|
||||
_AgentSquircleBorder(side: side ?? this.side);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.strokeInset);
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) =>
|
||||
_agentSquirclePath(rect.deflate(side.strokeInset));
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) =>
|
||||
_agentSquirclePath(rect);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
if (side.style == BorderStyle.none) return;
|
||||
canvas.drawPath(
|
||||
_agentSquirclePath(rect.inflate(side.strokeOffset / 2)),
|
||||
side.toPaint(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) => _AgentSquircleBorder(side: side.scale(t));
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is _AgentSquircleBorder && other.side == side;
|
||||
|
||||
@override
|
||||
int get hashCode => side.hashCode;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:buzz/features/agents/agent_avatar_picker.dart';
|
||||
import 'package:buzz/shared/agents/agent_avatar_catalog.dart';
|
||||
import 'package:buzz/shared/emoji/emoji_data.dart';
|
||||
import 'package:buzz/shared/emoji/emoji_data_provider.dart';
|
||||
import 'package:buzz/shared/widgets/agent_avatar_border.dart';
|
||||
import 'package:buzz/shared/widgets/avatar_shape.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -86,7 +86,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
(previewClip.clipper! as ShapeBorderClipper).shape,
|
||||
const AgentAvatarBorder(),
|
||||
avatarShape(isAgent: true),
|
||||
);
|
||||
expect(
|
||||
(tester
|
||||
|
||||
@@ -79,7 +79,7 @@ import 'package:buzz/shared/mentions/agent_identity_provider.dart';
|
||||
import 'package:buzz/shared/huddle/huddle.dart';
|
||||
import 'package:buzz/shared/relay/relay.dart';
|
||||
import 'package:buzz/shared/theme/theme.dart';
|
||||
import 'package:buzz/shared/widgets/agent_avatar_border.dart';
|
||||
import 'package:buzz/shared/widgets/avatar_shape.dart';
|
||||
import 'package:buzz/shared/widgets/app_list_card.dart';
|
||||
import 'package:buzz/shared/widgets/avatar_image.dart';
|
||||
import 'package:buzz/shared/widgets/frosted_app_bar.dart';
|
||||
@@ -11322,9 +11322,10 @@ void main() {
|
||||
)
|
||||
.first,
|
||||
);
|
||||
final shape = (frame.decoration! as ShapeDecoration).shape;
|
||||
expect(
|
||||
(frame.decoration! as ShapeDecoration).shape,
|
||||
isA<AgentAvatarBorder>(),
|
||||
shape,
|
||||
avatarShape(isAgent: true, side: (shape as OutlinedBorder).side),
|
||||
);
|
||||
}
|
||||
// As on desktop, the reply branch waits for a reply.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:buzz/shared/widgets/agent_avatar_border.dart';
|
||||
import 'package:buzz/shared/widgets/avatar_shape.dart';
|
||||
import 'package:buzz/shared/widgets/avatar_image.dart';
|
||||
import 'package:buzz/shared/emoji/native_emoji_glyph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -59,33 +59,10 @@ void main() {
|
||||
final clip = tester.widget<ClipPath>(find.byType(ClipPath));
|
||||
expect(
|
||||
(clip.clipper! as ShapeBorderClipper).shape,
|
||||
const AgentAvatarBorder(),
|
||||
avatarShape(isAgent: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('agent squircle follows the desktop mask geometry', () {
|
||||
// Desktop's avatar-squircle.svg passes through (.089, .089) at each
|
||||
// corner's midpoint, outside a circle and inside the square.
|
||||
final path = agentAvatarPath(const Rect.fromLTWH(0, 0, 100, 100));
|
||||
expect(path.getBounds(), const Rect.fromLTWH(0, 0, 100, 100));
|
||||
for (final corner in [
|
||||
const Offset(9.5, 9.5),
|
||||
const Offset(90.5, 9.5),
|
||||
const Offset(90.5, 90.5),
|
||||
const Offset(9.5, 90.5),
|
||||
]) {
|
||||
expect(path.contains(corner), isTrue, reason: '$corner');
|
||||
}
|
||||
for (final corner in [
|
||||
const Offset(8, 8),
|
||||
const Offset(92, 8),
|
||||
const Offset(92, 92),
|
||||
const Offset(8, 92),
|
||||
]) {
|
||||
expect(path.contains(corner), isFalse, reason: '$corner');
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('renders raccoon percent-encoded SVG data avatar', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
65
mobile/test/shared/widgets/avatar_shape_test.dart
Normal file
65
mobile/test/shared/widgets/avatar_shape_test.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:buzz/shared/widgets/avatar_shape.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('agent squircle follows the desktop mask geometry', () {
|
||||
// Desktop's avatar-squircle.svg passes through (.089, .089) at each
|
||||
// corner's midpoint, outside a circle and inside the square.
|
||||
const rect = Rect.fromLTWH(0, 0, 100, 100);
|
||||
final path = avatarPath(rect, isAgent: true);
|
||||
expect(path.getBounds(), rect);
|
||||
for (final (point, inside) in [
|
||||
(const Offset(9.5, 9.5), true),
|
||||
(const Offset(90.5, 9.5), true),
|
||||
(const Offset(90.5, 90.5), true),
|
||||
(const Offset(9.5, 90.5), true),
|
||||
(const Offset(8, 8), false),
|
||||
(const Offset(92, 8), false),
|
||||
(const Offset(92, 92), false),
|
||||
(const Offset(8, 92), false),
|
||||
]) {
|
||||
expect(path.contains(point), inside, reason: '$point');
|
||||
}
|
||||
// People keep circles: the squircle corner points fall outside them.
|
||||
expect(
|
||||
avatarPath(rect, isAgent: false).contains(const Offset(9.5, 9.5)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('avatar shapes come only from avatarShape', () {
|
||||
// Agent avatars drifted into circles and 30% rounded rects because each
|
||||
// surface picked its own shape. Shape decisions keyed on agent identity,
|
||||
// and person shapes on agent-only surfaces, must use avatar_shape.dart.
|
||||
final inlineAgentShape = RegExp(
|
||||
r'\b(isAgent|isBot|agent)\b\s*\?\s*(const\s+)?'
|
||||
r'(BoxShape|BorderRadius|Radius|CircleBorder|RoundedRectangleBorder|'
|
||||
r'RoundedSuperellipseBorder|ClipOval|ClipRRect|CircleAvatar)\b',
|
||||
);
|
||||
final inlineAgentClip = RegExp(
|
||||
r'if\s*\(\s*!?\s*(isAgent|isBot|agent)\s*\)\s*return\s+'
|
||||
r'(ClipOval|ClipRRect|CircleAvatar)\b',
|
||||
);
|
||||
final personShape = RegExp(r'\b(ClipOval|CircleAvatar|BoxShape\.circle)\b');
|
||||
final violations = <String>[];
|
||||
for (final file in Directory('lib').listSync(recursive: true)) {
|
||||
if (file is! File || !file.path.endsWith('.dart')) continue;
|
||||
final path = file.path.replaceAll(r'\', '/');
|
||||
if (path.endsWith('shared/widgets/avatar_shape.dart')) continue;
|
||||
final source = file.readAsStringSync();
|
||||
for (final match in [
|
||||
...inlineAgentShape.allMatches(source),
|
||||
...inlineAgentClip.allMatches(source),
|
||||
if (path.startsWith('lib/features/agents/'))
|
||||
...personShape.allMatches(source),
|
||||
]) {
|
||||
final line = '\n'.allMatches(source.substring(0, match.start)).length;
|
||||
violations.add('$path:${line + 1}: ${match[0]}');
|
||||
}
|
||||
}
|
||||
expect(violations, isEmpty);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user