feat/rewrite: убрал hero анимацию, добавил возможность просмотра тегов у стикеров
This commit is contained in:
@@ -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: () {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user