feat/rewrite: убрал hero анимацию, добавил возможность просмотра тегов у стикеров

This commit is contained in:
Jganenokk
2026-07-05 20:18:29 +07:00
parent 891e4a7957
commit 26eca7aae4
7 changed files with 116 additions and 164 deletions
@@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:komet/frontend/widgets/avatar_hero.dart';
import 'package:komet/frontend/widgets/glossy_pill.dart';
import 'package:komet/frontend/widgets/online_dot.dart';
@@ -90,33 +89,28 @@ class ChatHeaderRow extends StatelessWidget {
children: [
_withOnlineDot(
cs,
AvatarHero(
tag: 'chatAvatar_$chatId',
name: name,
imageUrl: imageUrl.isNotEmpty ? imageUrl : null,
child: imageUrl.isNotEmpty
? CircleAvatar(
radius: 22,
backgroundImage: CachedNetworkImageProvider(
imageUrl,
maxWidth: 144,
maxHeight: 144,
),
)
: CircleAvatar(
radius: 22,
backgroundColor: cs.primaryContainer,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
imageUrl.isNotEmpty
? CircleAvatar(
radius: 22,
backgroundImage: CachedNetworkImageProvider(
imageUrl,
maxWidth: 144,
maxHeight: 144,
),
)
: CircleAvatar(
radius: 22,
backgroundColor: cs.primaryContainer,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 16,
fontWeight: FontWeight.w600,
fontFamily: 'Outfit',
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
@@ -195,7 +189,11 @@ class ChatHeaderRow extends StatelessWidget {
),
if (showCall)
IconButton(
icon: Icon(Symbols.call, weight: 500, color: cs.onSurface),
icon: Icon(
Symbols.call,
weight: 500,
color: cs.onSurface,
),
onPressed: onCall,
),
Builder(
@@ -244,31 +242,26 @@ class ChatHeaderRow extends StatelessWidget {
children: [
_withOnlineDot(
cs,
AvatarHero(
tag: 'chatAvatar_$chatId',
name: name,
imageUrl: imageUrl.isNotEmpty ? imageUrl : null,
child: imageUrl.isNotEmpty
? CircleAvatar(
radius: 18,
backgroundImage: CachedNetworkImageProvider(
imageUrl,
maxWidth: 144,
maxHeight: 144,
),
)
: CircleAvatar(
radius: 18,
backgroundColor: cs.primaryContainer,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 12,
),
imageUrl.isNotEmpty
? CircleAvatar(
radius: 18,
backgroundImage: CachedNetworkImageProvider(
imageUrl,
maxWidth: 144,
maxHeight: 144,
),
)
: CircleAvatar(
radius: 18,
backgroundColor: cs.primaryContainer,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 12,
),
),
),
),
dotSize: 11,
),
const SizedBox(width: 12),
@@ -10,7 +10,6 @@ import '../../../core/utils/format.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/chat_info.dart';
import '../../../models/contact_info.dart';
import '../../widgets/avatar_hero.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/komet_avatar.dart';
@@ -58,7 +57,6 @@ class ChatInfoScreen extends StatefulWidget {
class _ChatInfoScreenState extends State<ChatInfoScreen> {
final _tabScrollController = ScrollController();
final _avatarHeroKey = GlobalKey();
int _myId = 0;
bool _isLoading = true;
@@ -255,7 +253,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 4),
_heroAvatar(),
_avatar(),
const SizedBox(height: 14),
Text(
widget.name,
@@ -1131,18 +1129,12 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
);
}
Widget _heroAvatar() {
return AvatarHero(
key: _avatarHeroKey,
tag: 'chatAvatar_${widget.chatId}',
Widget _avatar() {
return KometAvatar(
name: widget.name,
imageUrl: widget.imageUrl.isNotEmpty ? widget.imageUrl : null,
child: KometAvatar(
name: widget.name,
imageUrl: widget.imageUrl,
size: 96,
fontSize: 36,
),
imageUrl: widget.imageUrl,
size: 96,
fontSize: 36,
);
}
@@ -1159,15 +1151,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
return ListView(
padding: const EdgeInsets.fromLTRB(16, 60, 16, 0),
children: [
Center(
child: AvatarHero(
key: _avatarHeroKey,
tag: 'chatAvatar_${widget.chatId}',
name: widget.name,
imageUrl: widget.imageUrl.isNotEmpty ? widget.imageUrl : null,
child: block(96, 96, r: 48),
),
),
Center(child: block(96, 96, r: 48)),
const SizedBox(height: 14),
Center(child: block(160, 22, r: 8)),
const SizedBox(height: 8),
@@ -10,7 +10,6 @@ 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';
@@ -2252,7 +2251,7 @@ class _ChatListScreenState extends State<ChatListScreen>
messageItalic,
);
Widget avatarCircle = CircleAvatar(
final Widget avatarCircle = CircleAvatar(
radius: 24,
backgroundColor: cs.surfaceContainerHighest,
backgroundImage: imageUrl.isNotEmpty
@@ -2269,15 +2268,6 @@ class _ChatListScreenState extends State<ChatListScreen>
)
: 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: () {
-69
View File
@@ -1,69 +0,0 @@
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,
),
),
),
),
),
);
}
}
@@ -275,6 +275,7 @@ class _StickerPackSheetState extends State<_StickerPackSheet> {
peekId: item.id,
url: item.url,
lottieUrl: item.lottieUrl,
tags: item.tags,
child: Padding(
padding: const EdgeInsets.all(6),
child: StickerImage(
+51 -11
View File
@@ -9,8 +9,9 @@ import 'sticker_image.dart';
class _PeekData {
final String? url;
final String? lottieUrl;
final List<String> tags;
const _PeekData(this.url, this.lottieUrl);
const _PeekData(this.url, this.lottieUrl, this.tags);
}
class StickerPeekScope extends StatefulWidget {
@@ -71,7 +72,11 @@ class StickerPeekScopeState extends State<StickerPeekScope>
final cell = _hitTest(globalPos);
if (cell == null) return;
_currentId = cell.widget.peekId;
_current.value = _PeekData(cell.widget.url, cell.widget.lottieUrl);
_current.value = _PeekData(
cell.widget.url,
cell.widget.lottieUrl,
cell.widget.tags,
);
_showEntry();
_anim.forward(from: 0);
Haptics.medium();
@@ -82,7 +87,11 @@ class StickerPeekScopeState extends State<StickerPeekScope>
final cell = _hitTest(globalPos);
if (cell == null || cell.widget.peekId == _currentId) return;
_currentId = cell.widget.peekId;
_current.value = _PeekData(cell.widget.url, cell.widget.lottieUrl);
_current.value = _PeekData(
cell.widget.url,
cell.widget.lottieUrl,
cell.widget.tags,
);
Haptics.selection();
}
@@ -137,6 +146,7 @@ class StickerPeekable extends StatefulWidget {
final Object peekId;
final String? url;
final String? lottieUrl;
final List<String> tags;
final Widget child;
const StickerPeekable({
@@ -144,6 +154,7 @@ class StickerPeekable extends StatefulWidget {
required this.peekId,
this.url,
this.lottieUrl,
this.tags = const [],
required this.child,
});
@@ -214,14 +225,23 @@ class _PeekOverlay extends StatelessWidget {
valueListenable: data,
builder: (context, d, _) {
if (d == null) return const SizedBox.shrink();
return SizedBox(
width: previewSize,
height: previewSize,
child: StickerImage(
url: d.url,
lottieUrl: d.lottieUrl,
size: previewSize,
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (d.tags.isNotEmpty) ...[
_PeekTags(tags: d.tags),
const SizedBox(height: 18),
],
SizedBox(
width: previewSize,
height: previewSize,
child: StickerImage(
url: d.url,
lottieUrl: d.lottieUrl,
size: previewSize,
),
),
],
);
},
),
@@ -235,3 +255,23 @@ class _PeekOverlay extends StatelessWidget {
);
}
}
class _PeekTags extends StatelessWidget {
final List<String> tags;
const _PeekTags({required this.tags});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (final tag in tags)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(tag, style: const TextStyle(fontSize: 28)),
),
],
);
}
}
+13
View File
@@ -38,6 +38,7 @@ class StickerItem {
final int? setId;
final int? width;
final int? height;
final List<String> tags;
const StickerItem({
required this.id,
@@ -46,6 +47,7 @@ class StickerItem {
this.setId,
this.width,
this.height,
this.tags = const [],
});
bool get isAnimated => lottieUrl != null && lottieUrl!.isNotEmpty;
@@ -57,5 +59,16 @@ class StickerItem {
setId: map['setId'] as int?,
width: map['width'] as int?,
height: map['height'] as int?,
tags: _parseTags(map['tags']),
);
static List<String> _parseTags(dynamic raw) {
if (raw is! List) return const [];
final result = <String>[];
for (final e in raw) {
final s = e?.toString();
if (s != null && s.isNotEmpty) result.add(s);
}
return result;
}
}