feat: анимации с реанимации
This commit is contained in:
@@ -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<ChatInfoScreen> {
|
||||
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<ChatInfoScreen> {
|
||||
|
||||
// ─── 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<ChatInfoScreen> {
|
||||
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),
|
||||
|
||||
@@ -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<ChatListScreen>
|
||||
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<ChatListScreen>
|
||||
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,
|
||||
|
||||
@@ -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<ChatScreen>
|
||||
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<ChatScreen>
|
||||
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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user