From 646e05dd27d66af76467e1982dfb58253520c68a Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Fri, 3 Jul 2026 19:45:51 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=20=D1=80=D0=B5=D0=B0=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/chats/chat_info_screen.dart | 31 ++++-- .../screens/chats/chat_list_screen.dart | 44 +++++---- lib/frontend/screens/chats/chat_screen.dart | 99 +++++++++++-------- lib/frontend/widgets/avatar_hero.dart | 69 +++++++++++++ 4 files changed, 174 insertions(+), 69 deletions(-) create mode 100644 lib/frontend/widgets/avatar_hero.dart diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index d3e2b30..d07ccfd 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -7,6 +7,7 @@ import '../../../core/cache/info_cache.dart'; import '../../../core/config/app_show_extra_info.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/utils/format.dart'; +import '../../widgets/avatar_hero.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/komet_avatar.dart'; @@ -263,12 +264,7 @@ class _ChatInfoScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 4), - KometAvatar( - name: widget.name, - imageUrl: widget.imageUrl, - size: 96, - fontSize: 36, - ), + _heroAvatar(), const SizedBox(height: 14), Text( widget.name, @@ -1104,6 +1100,20 @@ class _ChatInfoScreenState extends State { // ─── SHIMMER ───────────────────────────────────────────────────────────── + Widget _heroAvatar() { + return AvatarHero( + tag: 'chatAvatar_${widget.chatId}', + name: widget.name, + imageUrl: widget.imageUrl.isNotEmpty ? widget.imageUrl : null, + child: KometAvatar( + name: widget.name, + imageUrl: widget.imageUrl, + size: 96, + fontSize: 36, + ), + ); + } + Widget _buildShimmer(ColorScheme cs) { Widget block(double w, double h, {double r = 8}) => Container( width: w, @@ -1117,7 +1127,14 @@ class _ChatInfoScreenState extends State { return ListView( padding: const EdgeInsets.fromLTRB(16, 60, 16, 0), children: [ - Center(child: block(96, 96, r: 48)), + Center( + child: AvatarHero( + tag: 'chatAvatar_${widget.chatId}', + name: widget.name, + imageUrl: widget.imageUrl.isNotEmpty ? widget.imageUrl : null, + child: block(96, 96, r: 48), + ), + ), const SizedBox(height: 14), Center(child: block(160, 22, r: 8)), const SizedBox(height: 8), diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 0a91e96..a755b87 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -11,6 +11,7 @@ import 'chat_screen.dart'; import 'search_screen.dart'; import 'create_group_flow.dart'; import '../../widgets/adaptive_shell.dart'; +import '../../widgets/avatar_hero.dart'; import '../../widgets/online_dot.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; @@ -2300,6 +2301,28 @@ class _ChatListScreenState extends State messageItalic, ); + Widget avatarCircle = CircleAvatar( + radius: 24, + backgroundColor: cs.surfaceContainerHighest, + backgroundImage: imageUrl.isNotEmpty + ? CachedNetworkImageProvider(imageUrl, maxWidth: 144, maxHeight: 144) + : null, + child: imageUrl.isEmpty + ? Text( + name.isNotEmpty ? name[0].toUpperCase() : '?', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 20), + ) + : null, + ); + if (widget.onChatSelected == null) { + avatarCircle = AvatarHero( + tag: 'chatAvatar_$id', + name: name, + imageUrl: imageUrl.isNotEmpty ? imageUrl : null, + child: avatarCircle, + ); + } + return InkWell( key: ValueKey('chat_$id'), onTap: () { @@ -2353,26 +2376,7 @@ class _ChatListScreenState extends State children: [ Stack( children: [ - CircleAvatar( - radius: 24, - backgroundColor: cs.surfaceContainerHighest, - backgroundImage: imageUrl.isNotEmpty - ? CachedNetworkImageProvider( - imageUrl, - maxWidth: 144, - maxHeight: 144, - ) - : null, - child: imageUrl.isEmpty - ? Text( - name.isNotEmpty ? name[0].toUpperCase() : '?', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 20, - ), - ) - : null, - ), + avatarCircle, if (isSelected) Positioned( right: -2, diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 5c1648f..94c3c85 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -54,6 +54,7 @@ import '../../../models/attachment.dart'; import '../../../models/sticker.dart'; import '../../commands/command_registry.dart'; import '../../commands/slash_command.dart'; +import '../../widgets/avatar_hero.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/rich_message_controller.dart'; import '../../../core/utils/text_format.dart'; @@ -2116,30 +2117,37 @@ class _ChatScreenState extends State children: [ _withOnlineDot( cs, - widget.imageUrl.isNotEmpty - ? CircleAvatar( - radius: 22, - backgroundImage: CachedNetworkImageProvider( - widget.imageUrl, - maxWidth: 144, - maxHeight: 144, - ), - ) - : CircleAvatar( - radius: 22, - backgroundColor: cs.primaryContainer, - child: Text( - widget.name.isNotEmpty - ? widget.name[0].toUpperCase() - : '?', - style: TextStyle( - color: cs.onPrimaryContainer, - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + AvatarHero( + tag: 'chatAvatar_${widget.chatId}', + name: widget.name, + imageUrl: widget.imageUrl.isNotEmpty + ? widget.imageUrl + : null, + child: widget.imageUrl.isNotEmpty + ? CircleAvatar( + radius: 22, + backgroundImage: CachedNetworkImageProvider( + widget.imageUrl, + maxWidth: 144, + maxHeight: 144, + ), + ) + : CircleAvatar( + radius: 22, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty + ? widget.name[0].toUpperCase() + : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), ), ), - ), + ), ), const SizedBox(width: 12), Expanded( @@ -2276,28 +2284,35 @@ class _ChatScreenState extends State children: [ _withOnlineDot( cs, - widget.imageUrl.isNotEmpty - ? CircleAvatar( - radius: 18, - backgroundImage: CachedNetworkImageProvider( - widget.imageUrl, - maxWidth: 144, - maxHeight: 144, - ), - ) - : CircleAvatar( - radius: 18, - backgroundColor: cs.primaryContainer, - child: Text( - widget.name.isNotEmpty - ? widget.name[0].toUpperCase() - : '?', - style: TextStyle( - color: cs.onPrimaryContainer, - fontSize: 12, + AvatarHero( + tag: 'chatAvatar_${widget.chatId}', + name: widget.name, + imageUrl: widget.imageUrl.isNotEmpty + ? widget.imageUrl + : null, + child: widget.imageUrl.isNotEmpty + ? CircleAvatar( + radius: 18, + backgroundImage: CachedNetworkImageProvider( + widget.imageUrl, + maxWidth: 144, + maxHeight: 144, + ), + ) + : CircleAvatar( + radius: 18, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty + ? widget.name[0].toUpperCase() + : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 12, + ), ), ), - ), + ), dotSize: 11, ), const SizedBox(width: 12), diff --git a/lib/frontend/widgets/avatar_hero.dart b/lib/frontend/widgets/avatar_hero.dart new file mode 100644 index 0000000..1c94132 --- /dev/null +++ b/lib/frontend/widgets/avatar_hero.dart @@ -0,0 +1,69 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class AvatarHero extends StatelessWidget { + final Object tag; + final String name; + final String? imageUrl; + final Widget child; + + const AvatarHero({ + super.key, + required this.tag, + required this.name, + required this.imageUrl, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Hero( + tag: tag, + flightShuttleBuilder: (context, animation, direction, fromContext, toContext) => + _AvatarHeroFlight(name: name, imageUrl: imageUrl), + child: child, + ); + } +} + +class _AvatarHeroFlight extends StatelessWidget { + final String name; + final String? imageUrl; + + const _AvatarHeroFlight({required this.name, required this.imageUrl}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final url = imageUrl; + final hasImage = url != null && url.isNotEmpty; + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.primaryContainer, + ), + child: hasImage + ? Image( + image: CachedNetworkImageProvider(url), + fit: BoxFit.cover, + gaplessPlayback: true, + ) + : Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(10), + child: Text( + name.isNotEmpty ? name[0].toUpperCase() : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ); + } +}