feat: еще стикеры
This commit is contained in:
@@ -828,6 +828,28 @@ class MessagesModule {
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<bool> sendLinkMessage(int chatId, String url) async {
|
||||
final message = <String, dynamic>{
|
||||
'text': url,
|
||||
'cid': DateTime.now().millisecondsSinceEpoch * -1,
|
||||
'elements': [
|
||||
{
|
||||
'type': 'LINK',
|
||||
'from': 0,
|
||||
'length': url.length,
|
||||
'attributes': {'url': url},
|
||||
},
|
||||
],
|
||||
'attaches': [],
|
||||
};
|
||||
final response = await _api.sendRequest(Opcode.msgSend, {
|
||||
'chatId': chatId,
|
||||
'message': message,
|
||||
'notify': true,
|
||||
});
|
||||
return response.isOk;
|
||||
}
|
||||
|
||||
/// Загружает отложенные (запланированные) сообщения чата.
|
||||
///
|
||||
/// В отличие от обычной истории, отложенные сообщения не сохраняются
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import '../api.dart';
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../../models/sticker.dart';
|
||||
|
||||
class StickersModule {
|
||||
final Api _api;
|
||||
|
||||
StickersModule(this._api);
|
||||
StickersModule(this._api) {
|
||||
_api.pushStream
|
||||
.where((p) => p.opcode == Opcode.notifAssetsUpdate)
|
||||
.listen(_handleAssetsPush);
|
||||
}
|
||||
|
||||
final Map<int, StickerSet> _sets = {};
|
||||
final Map<int, StickerItem> _stickers = {};
|
||||
@@ -15,6 +20,7 @@ class StickersModule {
|
||||
List<int> _recentStickerIds = [];
|
||||
|
||||
Future<void>? _loading;
|
||||
Future<void>? _favoritesLoading;
|
||||
|
||||
List<StickerSet> get sets =>
|
||||
_orderedSetIds.map((id) => _sets[id]).whereType<StickerSet>().toList();
|
||||
@@ -22,6 +28,8 @@ class StickersModule {
|
||||
List<int> get favoriteSetIds => _favoriteSetIds;
|
||||
List<int> get recentStickerIds => _recentStickerIds;
|
||||
StickerItem? cachedSticker(int id) => _stickers[id];
|
||||
StickerSet? cachedSet(int id) => _sets[id];
|
||||
bool isFavorite(int setId) => _favoriteSetIds.contains(setId);
|
||||
|
||||
Future<void> ensureLoaded() {
|
||||
return _loading ??= _loadSections().catchError((Object e) {
|
||||
@@ -30,6 +38,32 @@ class StickersModule {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> ensureFavoritesLoaded() {
|
||||
return _favoritesLoading ??= _loadFavorites().catchError((Object e) {
|
||||
_favoritesLoading = null;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadFavorites() async {
|
||||
final favIds = <int>[];
|
||||
final favResp = await _api.sendRequest(Opcode.assetsUpdate, {
|
||||
'type': 'FAVORITE_STICKER',
|
||||
'sync': 0,
|
||||
});
|
||||
if (favResp.isOk && favResp.payload is Map) {
|
||||
final sections = favResp.payload['sections'];
|
||||
if (sections is List) {
|
||||
for (final s in sections) {
|
||||
if (s is Map && s['id'] == 'FAVORITE_STICKER_SETS') {
|
||||
_appendIntList(favIds, s['stickerSets']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_favoriteSetIds = favIds;
|
||||
}
|
||||
|
||||
Future<void> _loadSections() async {
|
||||
final newSetIds = <int>[];
|
||||
int marker = 0;
|
||||
@@ -70,26 +104,11 @@ class StickersModule {
|
||||
marker = m is int ? m : 0;
|
||||
}
|
||||
|
||||
final favIds = <int>[];
|
||||
final favResp = await _api.sendRequest(Opcode.assetsUpdate, {
|
||||
'type': 'FAVORITE_STICKER',
|
||||
'sync': 0,
|
||||
});
|
||||
if (favResp.isOk && favResp.payload is Map) {
|
||||
final sections = favResp.payload['sections'];
|
||||
if (sections is List) {
|
||||
for (final s in sections) {
|
||||
if (s is Map && s['id'] == 'FAVORITE_STICKER_SETS') {
|
||||
_appendIntList(favIds, s['stickerSets']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_favoriteSetIds = favIds;
|
||||
await ensureFavoritesLoaded();
|
||||
|
||||
final ordered = <int>[];
|
||||
final seen = <int>{};
|
||||
for (final id in [...favIds, ...newSetIds]) {
|
||||
for (final id in [..._favoriteSetIds, ...newSetIds]) {
|
||||
if (seen.add(id)) ordered.add(id);
|
||||
}
|
||||
_orderedSetIds = ordered;
|
||||
@@ -140,6 +159,74 @@ class StickersModule {
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<StickerSet?> ensureSet(int setId) async {
|
||||
await _ensureSetMetas([setId]);
|
||||
return _sets[setId];
|
||||
}
|
||||
|
||||
void cacheSet(StickerSet set) => _sets[set.id] = set;
|
||||
|
||||
Future<StickerSet?> resolveSetByLink(String link) async {
|
||||
final resp = await _api.sendRequest(Opcode.linkInfo, {'link': link});
|
||||
if (!resp.isOk || resp.payload is! Map) return null;
|
||||
final raw = resp.payload['stickerSet'];
|
||||
if (raw is! Map || raw['id'] is! int) return null;
|
||||
final set = StickerSet.fromMap(raw);
|
||||
_sets[set.id] = set;
|
||||
return set;
|
||||
}
|
||||
|
||||
Future<int?> resolveSetId(int stickerId) async {
|
||||
await ensureStickers([stickerId]);
|
||||
return _stickers[stickerId]?.setId;
|
||||
}
|
||||
|
||||
Future<bool> favoriteSet(int setId) async {
|
||||
final resp = await _api.sendRequest(Opcode.assetsAdd, {
|
||||
'type': 'FAVORITE_STICKER_SET',
|
||||
'id': setId,
|
||||
});
|
||||
final ok = resp.isOk && resp.payload is Map && resp.payload['success'] == true;
|
||||
if (ok) _markFavorite(setId, true);
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<bool> unfavoriteSet(int setId) async {
|
||||
final resp = await _api.sendRequest(Opcode.assetsRemove, {
|
||||
'type': 'FAVORITE_STICKER_SET',
|
||||
'ids': [setId],
|
||||
});
|
||||
final ok = resp.isOk && resp.payload is Map && resp.payload['success'] == true;
|
||||
if (ok) _markFavorite(setId, false);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void _handleAssetsPush(Packet push) {
|
||||
final payload = push.payload;
|
||||
if (payload is! Map) return;
|
||||
if (payload['type'] != 'FAVORITE_STICKER_SET') return;
|
||||
final id = payload['id'];
|
||||
if (id is! int) return;
|
||||
switch (payload['updateType']) {
|
||||
case 'ADDED':
|
||||
_markFavorite(id, true);
|
||||
case 'REMOVED':
|
||||
_markFavorite(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _markFavorite(int setId, bool favorite) {
|
||||
if (favorite) {
|
||||
if (!_favoriteSetIds.contains(setId)) {
|
||||
_favoriteSetIds = [setId, ..._favoriteSetIds];
|
||||
}
|
||||
} else {
|
||||
if (_favoriteSetIds.contains(setId)) {
|
||||
_favoriteSetIds = _favoriteSetIds.where((id) => id != setId).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _parseRecents(dynamic list) {
|
||||
if (list is! List) return;
|
||||
final ids = <int>[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
enum MaxLinkKind { call, invite, user, content, public, auth }
|
||||
enum MaxLinkKind { call, invite, user, content, public, auth, stickerSet }
|
||||
|
||||
class MaxLink {
|
||||
final MaxLinkKind kind;
|
||||
@@ -49,6 +49,10 @@ class MaxLink {
|
||||
return segments.length >= 2 ? MaxLink(MaxLinkKind.user, url) : null;
|
||||
case 'c':
|
||||
return segments.length >= 3 ? MaxLink(MaxLinkKind.content, url) : null;
|
||||
case 'stickerset':
|
||||
return segments.length >= 2
|
||||
? MaxLink(MaxLinkKind.stickerSet, url)
|
||||
: null;
|
||||
}
|
||||
|
||||
if (_reserved.contains(segments.first.toLowerCase())) return null;
|
||||
|
||||
@@ -62,6 +62,7 @@ import '../../widgets/message_actions_overlay.dart';
|
||||
import '../../widgets/attachment_panel.dart';
|
||||
import '../../widgets/attachment/attachment_sheet.dart';
|
||||
import '../../widgets/sticker_panel.dart';
|
||||
import '../../widgets/sticker_pack_sheet.dart';
|
||||
import '../../widgets/swipe_to_pop.dart';
|
||||
import '../../widgets/schedule_time_picker.dart';
|
||||
import 'scheduled_messages_screen.dart';
|
||||
@@ -3105,6 +3106,19 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void _openStickerPack(StickerAttachment sticker) {
|
||||
final stickerId = int.tryParse(sticker.stickerId ?? '');
|
||||
if (stickerId == null) {
|
||||
showCustomNotification(context, 'Стикерпак недоступен');
|
||||
return;
|
||||
}
|
||||
showStickerPackSheet(
|
||||
context,
|
||||
stickerId: stickerId,
|
||||
knownSetId: int.tryParse(sticker.stickerPackId ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
void _jumpToMessage(String messageId) {
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
if (index == -1) {
|
||||
@@ -3452,6 +3466,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
uploadProgress: _photoProgressFor(message),
|
||||
onReplyTap: _jumpToMessage,
|
||||
onAvatarTap: _openSenderProfile,
|
||||
onStickerTap: _openStickerPack,
|
||||
);
|
||||
|
||||
final canReport = !isMe && !message.isControl;
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../screens/contacts/contact_profile_screen.dart';
|
||||
import 'call_link_handler.dart';
|
||||
import 'confirm_dialog.dart';
|
||||
import 'custom_notification.dart';
|
||||
import 'sticker_pack_sheet.dart';
|
||||
import 'swipe_route.dart';
|
||||
import 'web_qr_login.dart';
|
||||
|
||||
@@ -26,6 +27,10 @@ Future<bool> tryHandleMaxLink(BuildContext context, String url) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (link.kind == MaxLinkKind.stickerSet) {
|
||||
return _openStickerSet(context, link.url);
|
||||
}
|
||||
|
||||
final resolved = await LinkModule.resolve(api, link.url);
|
||||
if (!context.mounted) return true;
|
||||
|
||||
@@ -44,6 +49,26 @@ Future<bool> tryHandleMaxLink(BuildContext context, String url) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _openStickerSet(BuildContext context, String url) async {
|
||||
final path = url
|
||||
.replaceFirst(
|
||||
RegExp(r'^https?://(?:www\.)?max\.ru/', caseSensitive: false),
|
||||
'',
|
||||
)
|
||||
.split('?')
|
||||
.first
|
||||
.split('#')
|
||||
.first;
|
||||
final set = await stickersModule.resolveSetByLink(path);
|
||||
if (!context.mounted) return true;
|
||||
if (set == null) {
|
||||
showCustomNotification(context, 'Стикерпак недоступен');
|
||||
return true;
|
||||
}
|
||||
await showStickerPackSheet(context, knownSetId: set.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void _openContact(BuildContext context, Map<dynamic, dynamic> contact) {
|
||||
final id = contact['id'];
|
||||
if (id is! int) {
|
||||
|
||||
@@ -111,6 +111,7 @@ class MessageBubble extends StatelessWidget {
|
||||
final ValueListenable<List<double>>? uploadProgress;
|
||||
final void Function(String messageId)? onReplyTap;
|
||||
final void Function(int senderId)? onAvatarTap;
|
||||
final void Function(StickerAttachment sticker)? onStickerTap;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
@@ -125,6 +126,7 @@ class MessageBubble extends StatelessWidget {
|
||||
this.uploadProgress,
|
||||
this.onReplyTap,
|
||||
this.onAvatarTap,
|
||||
this.onStickerTap,
|
||||
});
|
||||
|
||||
bool _computeHasPhotoWithCaption() {
|
||||
@@ -2335,7 +2337,7 @@ class MessageBubble extends StatelessWidget {
|
||||
final preview = sticker.previewData ?? '';
|
||||
final imageUrl = url.isNotEmpty ? url : preview;
|
||||
|
||||
return ClipRRect(
|
||||
final Widget image = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(photoBorderRadius),
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -2356,6 +2358,14 @@ class MessageBubble extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final onTap = onStickerTap;
|
||||
if (onTap == null || sticker is! StickerAttachment) return image;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => onTap(sticker),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactAttachment(_BubbleCtx ctx, MessageAttachment contact) {
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../core/storage/app_database.dart';
|
||||
import '../../main.dart' show stickersModule, messagesModule;
|
||||
import '../../models/sticker.dart';
|
||||
import '../screens/chats/forward_picker_screen.dart';
|
||||
import 'custom_notification.dart';
|
||||
|
||||
enum _PackAction { forward, copyLink }
|
||||
|
||||
Future<void> showStickerPackSheet(
|
||||
BuildContext context, {
|
||||
int? stickerId,
|
||||
int? knownSetId,
|
||||
}) {
|
||||
assert(stickerId != null || knownSetId != null);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _StickerPackSheet(
|
||||
stickerId: stickerId,
|
||||
knownSetId: knownSetId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _StickerPackSheet extends StatefulWidget {
|
||||
final int? stickerId;
|
||||
final int? knownSetId;
|
||||
|
||||
const _StickerPackSheet({this.stickerId, this.knownSetId});
|
||||
|
||||
@override
|
||||
State<_StickerPackSheet> createState() => _StickerPackSheetState();
|
||||
}
|
||||
|
||||
class _StickerPackSheetState extends State<_StickerPackSheet> {
|
||||
bool _loading = true;
|
||||
bool _busy = false;
|
||||
bool _isFavorite = false;
|
||||
Object? _error;
|
||||
StickerSet? _set;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final setId = widget.knownSetId ??
|
||||
(widget.stickerId != null
|
||||
? await stickersModule.resolveSetId(widget.stickerId!)
|
||||
: null);
|
||||
if (setId == null) throw Exception('no set');
|
||||
final setFuture = stickersModule.ensureSet(setId);
|
||||
final favoritesFuture = stickersModule.ensureFavoritesLoaded();
|
||||
final set = await setFuture;
|
||||
await favoritesFuture;
|
||||
if (set == null) throw Exception('no meta');
|
||||
await stickersModule.ensureStickers(set.stickerIds);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_set = set;
|
||||
_isFavorite = stickersModule.isFavorite(set.id);
|
||||
_loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggle() async {
|
||||
final set = _set;
|
||||
if (set == null || _busy) return;
|
||||
setState(() => _busy = true);
|
||||
final wasFavorite = _isFavorite;
|
||||
try {
|
||||
final ok = wasFavorite
|
||||
? await stickersModule.unfavoriteSet(set.id)
|
||||
: await stickersModule.favoriteSet(set.id);
|
||||
if (!mounted) return;
|
||||
if (ok) {
|
||||
setState(() => _isFavorite = !wasFavorite);
|
||||
showCustomNotification(
|
||||
context,
|
||||
wasFavorite ? 'Стикерпак удалён' : 'Стикерпак добавлен',
|
||||
);
|
||||
} else {
|
||||
showCustomNotification(context, 'Не удалось выполнить действие');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showCustomNotification(context, 'Ошибка: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _copyLink(StickerSet set) {
|
||||
final link = set.link;
|
||||
if (link == null || link.isEmpty) {
|
||||
showCustomNotification(context, 'Ссылка недоступна');
|
||||
return;
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: link));
|
||||
showCustomNotification(context, 'Ссылка скопирована');
|
||||
}
|
||||
|
||||
Future<void> _forward(StickerSet set) async {
|
||||
final link = set.link;
|
||||
if (link == null || link.isEmpty) {
|
||||
showCustomNotification(context, 'Ссылка недоступна');
|
||||
return;
|
||||
}
|
||||
final profile = await AppDatabase.loadActiveProfile();
|
||||
if (!mounted) return;
|
||||
final target = await showForwardPicker(
|
||||
context: context,
|
||||
accountId: profile?.id ?? 0,
|
||||
);
|
||||
if (target == null || !mounted) return;
|
||||
final ok = await messagesModule.sendLinkMessage(target.chatId, link);
|
||||
if (!mounted) return;
|
||||
showCustomNotification(
|
||||
context,
|
||||
ok ? 'Переслано в «${target.name}»' : 'Не удалось переслать',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final height = MediaQuery.sizeOf(context).height * 0.78;
|
||||
|
||||
return Container(
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Expanded(child: _buildBody(cs)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(ColorScheme cs) {
|
||||
if (_loading) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.4, color: cs.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
final set = _set;
|
||||
if (_error != null || set == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Стикерпак недоступен',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_buildHeader(cs, set),
|
||||
Expanded(child: _buildGrid(cs, set)),
|
||||
_buildActionButton(cs),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme cs, StickerSet set) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 6, 8, 14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
set.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_pluralStickers(set.stickerIds.length),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildMenu(cs, set),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenu(ColorScheme cs, StickerSet set) {
|
||||
return PopupMenuButton<_PackAction>(
|
||||
icon: Icon(Symbols.more_horiz, color: cs.onSurfaceVariant),
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
onSelected: (action) {
|
||||
switch (action) {
|
||||
case _PackAction.forward:
|
||||
_forward(set);
|
||||
case _PackAction.copyLink:
|
||||
_copyLink(set);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _PackAction.forward,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 20, color: cs.onSurface),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Переслать'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _PackAction.copyLink,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.link, size: 20, color: cs.onSurface),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Скопировать ссылку'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGrid(ColorScheme cs, StickerSet set) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
),
|
||||
itemCount: set.stickerIds.length,
|
||||
itemBuilder: (context, i) {
|
||||
final item = stickersModule.cachedSticker(set.stickerIds[i]);
|
||||
if (item == null || item.url.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: item.url,
|
||||
fit: BoxFit.contain,
|
||||
memCacheWidth: 220,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
placeholder: (_, _) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(ColorScheme cs) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _isFavorite
|
||||
? cs.surfaceContainerHighest
|
||||
: cs.primary,
|
||||
foregroundColor: _isFavorite ? cs.onSurface : cs.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
onPressed: _busy ? null : _toggle,
|
||||
child: _busy
|
||||
? SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.4,
|
||||
color: _isFavorite ? cs.onSurface : cs.onPrimary,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isFavorite ? 'Убрать' : 'Добавить',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _pluralStickers(int n) {
|
||||
final mod100 = n % 100;
|
||||
final mod10 = n % 10;
|
||||
if (mod100 >= 11 && mod100 <= 14) return '$n стикеров';
|
||||
if (mod10 == 1) return '$n стикер';
|
||||
if (mod10 >= 2 && mod10 <= 4) return '$n стикера';
|
||||
return '$n стикеров';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user