feat: стикеры

This commit is contained in:
Jganenokk
2026-07-03 00:45:36 +07:00
parent a25e21277d
commit 0e27f141a7
6 changed files with 757 additions and 5 deletions
+37
View File
@@ -1453,6 +1453,43 @@ class MessagesModule {
return null; return null;
} }
void sendTyping(int chatId, String type) {
unawaited(() async {
try {
await _api.sendRequest(Opcode.msgTyping, {
'chatId': chatId,
'type': type,
});
} catch (_) {}
}());
}
Future<Map<String, dynamic>?> sendStickerMessage(
int chatId,
int stickerId, {
bool notify = true,
}) async {
final payload = {
'chatId': chatId,
'message': {
'cid': DateTime.now().millisecondsSinceEpoch * -1,
'attaches': [
{'_type': 'STICKER', 'stickerId': stickerId},
],
},
'notify': notify,
};
final response = await _api.sendRequest(Opcode.msgSend, payload);
if (!response.isOk) return null;
final data = response.payload;
if (data is Map) {
final msg = data['message'];
if (msg is Map) return Map<String, dynamic>.from(msg);
}
return null;
}
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async { Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
try { try {
final response = await _api.sendRequest(Opcode.fileDownload, { final response = await _api.sendRequest(Opcode.fileDownload, {
+167
View File
@@ -0,0 +1,167 @@
import '../api.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/utils/logger.dart';
import '../../models/sticker.dart';
class StickersModule {
final Api _api;
StickersModule(this._api);
final Map<int, StickerSet> _sets = {};
final Map<int, StickerItem> _stickers = {};
List<int> _orderedSetIds = [];
List<int> _favoriteSetIds = [];
List<int> _recentStickerIds = [];
Future<void>? _loading;
List<StickerSet> get sets =>
_orderedSetIds.map((id) => _sets[id]).whereType<StickerSet>().toList();
List<int> get favoriteSetIds => _favoriteSetIds;
List<int> get recentStickerIds => _recentStickerIds;
StickerItem? cachedSticker(int id) => _stickers[id];
Future<void> ensureLoaded() {
return _loading ??= _loadSections().catchError((Object e) {
_loading = null;
throw e;
});
}
Future<void> _loadSections() async {
final newSetIds = <int>[];
int marker = 0;
final stickerResp = await _api.sendRequest(Opcode.assetsUpdate, {
'type': 'STICKER',
'sync': 0,
});
if (stickerResp.isOk && stickerResp.payload is Map) {
final sections = stickerResp.payload['sections'];
if (sections is List) {
for (final s in sections) {
if (s is! Map) continue;
if (s['id'] == 'NEW_STICKER_SETS') {
_appendIntList(newSetIds, s['stickerSets']);
final m = s['marker'];
if (m is int) marker = m;
} else if (s['type'] == 'RECENTS') {
_parseRecents(s['recentsList']);
}
}
}
}
var guard = 0;
while (marker != 0 && guard < 50) {
guard++;
final page = await _api.sendRequest(Opcode.assetsGet, {
'sectionId': 'NEW_STICKER_SETS',
'from': marker,
'count': 100,
});
if (!page.isOk || page.payload is! Map) break;
final before = newSetIds.length;
_appendIntList(newSetIds, page.payload['stickerSets']);
if (newSetIds.length == before) break;
final m = page.payload['marker'];
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;
final ordered = <int>[];
final seen = <int>{};
for (final id in [...favIds, ...newSetIds]) {
if (seen.add(id)) ordered.add(id);
}
_orderedSetIds = ordered;
logger.i('Стикеры: ${ordered.length} паков, ${_recentStickerIds.length} недавних');
await _ensureSetMetas(ordered);
}
Future<void> _ensureSetMetas(List<int> ids) async {
final missing = ids.where((id) => !_sets.containsKey(id)).toList();
for (final batch in _chunk(missing, 100)) {
final resp = await _api.sendRequest(Opcode.assetsGetByIds, {
'type': 'STICKER_SET',
'ids': batch,
});
if (!resp.isOk || resp.payload is! Map) continue;
final list = resp.payload['stickerSets'];
if (list is! List) continue;
for (final e in list) {
if (e is Map && e['id'] is int) {
final set = StickerSet.fromMap(e);
_sets[set.id] = set;
}
}
}
}
Future<List<StickerItem>> ensureStickers(List<int> stickerIds) async {
final missing = stickerIds.where((id) => !_stickers.containsKey(id)).toList();
for (final batch in _chunk(missing, 100)) {
final resp = await _api.sendRequest(Opcode.assetsGetByIds, {
'type': 'STICKER',
'ids': batch,
});
if (!resp.isOk || resp.payload is! Map) continue;
final list = resp.payload['stickers'];
if (list is! List) continue;
for (final e in list) {
if (e is Map && e['id'] is int) {
final item = StickerItem.fromMap(e);
_stickers[item.id] = item;
}
}
}
return stickerIds
.map((id) => _stickers[id])
.whereType<StickerItem>()
.toList();
}
void _parseRecents(dynamic list) {
if (list is! List) return;
final ids = <int>[];
for (final e in list) {
if (e is Map && e['type'] == 'STICKER') {
final sid = e['stickerId'] ?? e['id'];
if (sid is int) ids.add(sid);
}
}
_recentStickerIds = ids;
}
void _appendIntList(List<int> target, dynamic raw) {
if (raw is! List) return;
for (final e in raw) {
if (e is int) target.add(e);
}
}
Iterable<List<T>> _chunk<T>(List<T> list, int size) sync* {
for (var i = 0; i < list.length; i += size) {
yield list.sublist(i, i + size > list.length ? list.length : i + size);
}
}
}
+105 -5
View File
@@ -49,6 +49,7 @@ import '../../../core/config/app_commands.dart';
import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_visual_style.dart';
import '../../../core/config/komet_settings.dart'; import '../../../core/config/komet_settings.dart';
import '../../../models/attachment.dart'; import '../../../models/attachment.dart';
import '../../../models/sticker.dart';
import '../../commands/command_registry.dart'; import '../../commands/command_registry.dart';
import '../../commands/slash_command.dart'; import '../../commands/slash_command.dart';
import '../../widgets/glossy_pill.dart'; import '../../widgets/glossy_pill.dart';
@@ -60,6 +61,7 @@ import '../../widgets/theme_reveal.dart';
import '../../widgets/message_actions_overlay.dart'; import '../../widgets/message_actions_overlay.dart';
import '../../widgets/attachment_panel.dart'; import '../../widgets/attachment_panel.dart';
import '../../widgets/attachment/attachment_sheet.dart'; import '../../widgets/attachment/attachment_sheet.dart';
import '../../widgets/sticker_panel.dart';
import '../../widgets/swipe_to_pop.dart'; import '../../widgets/swipe_to_pop.dart';
import '../../widgets/schedule_time_picker.dart'; import '../../widgets/schedule_time_picker.dart';
import 'scheduled_messages_screen.dart'; import 'scheduled_messages_screen.dart';
@@ -206,6 +208,10 @@ class _ChatScreenState extends State<ChatScreen>
final ValueNotifier<bool> _hasText = ValueNotifier(false); final ValueNotifier<bool> _hasText = ValueNotifier(false);
bool _isLoading = true; bool _isLoading = true;
final ValueNotifier<bool> _showAttachmentPanel = ValueNotifier(false); final ValueNotifier<bool> _showAttachmentPanel = ValueNotifier(false);
final ValueNotifier<bool> _showStickerPanel = ValueNotifier(false);
double _stickerPanelHeight = 300;
Timer? _stickerTypingTimer;
late final AnimationController _stickerAnim;
final ValueNotifier<_UploadStatus> _uploadStatus = ValueNotifier( final ValueNotifier<_UploadStatus> _uploadStatus = ValueNotifier(
const _UploadStatus(), const _UploadStatus(),
); );
@@ -337,6 +343,8 @@ class _ChatScreenState extends State<ChatScreen>
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
ChatsModule.chatsChanged.addListener(_onChatsBump); ChatsModule.chatsChanged.addListener(_onChatsBump);
_messageController.addListener(_onTextChanged); _messageController.addListener(_onTextChanged);
_messageFocusNode.addListener(_onComposerFocusChanged);
_showStickerPanel.addListener(_onStickerPanelToggle);
_scrollController.addListener(_onScrollForDate); _scrollController.addListener(_onScrollForDate);
_scrollController.addListener(_maybeLoadMoreHistory); _scrollController.addListener(_maybeLoadMoreHistory);
AppVisualStyle.current.addListener(_onVisualStyleChanged); AppVisualStyle.current.addListener(_onVisualStyleChanged);
@@ -349,6 +357,11 @@ class _ChatScreenState extends State<ChatScreen>
duration: const Duration(milliseconds: 320), duration: const Duration(milliseconds: 320),
reverseDuration: const Duration(milliseconds: 240), reverseDuration: const Duration(milliseconds: 240),
); );
_stickerAnim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 240),
reverseDuration: const Duration(milliseconds: 200),
);
_showAttachmentPanel.addListener(_onAttachPanelToggle); _showAttachmentPanel.addListener(_onAttachPanelToggle);
_commandAnim = AnimationController( _commandAnim = AnimationController(
vsync: this, vsync: this,
@@ -897,7 +910,12 @@ class _ChatScreenState extends State<ChatScreen>
_selectedIds.dispose(); _selectedIds.dispose();
_commandMatches.dispose(); _commandMatches.dispose();
_messageController.dispose(); _messageController.dispose();
_messageFocusNode.removeListener(_onComposerFocusChanged);
_messageFocusNode.dispose(); _messageFocusNode.dispose();
_stickerTypingTimer?.cancel();
_stickerAnim.dispose();
_showStickerPanel.removeListener(_onStickerPanelToggle);
_showStickerPanel.dispose();
_scrollController.dispose(); _scrollController.dispose();
_shimmerStartTimer?.cancel(); _shimmerStartTimer?.cancel();
_shimmerController.dispose(); _shimmerController.dispose();
@@ -1383,6 +1401,7 @@ class _ChatScreenState extends State<ChatScreen>
}, },
), ),
_buildInputArea(context), _buildInputArea(context),
_buildStickerPanel(context),
], ],
), ),
), ),
@@ -4507,11 +4526,15 @@ class _ChatScreenState extends State<ChatScreen>
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Icon( GestureDetector(
Symbols.face, behavior: HitTestBehavior.opaque,
color: mutedIcon, onTap: _toggleStickerPanel,
size: 24, child: Icon(
weight: 400, Symbols.face,
color: mutedIcon,
size: 24,
weight: 400,
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@@ -5259,6 +5282,83 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
void _toggleStickerPanel() {
if (_showStickerPanel.value) {
_showStickerPanel.value = false;
_messageFocusNode.requestFocus();
return;
}
final keyboard = MediaQuery.viewInsetsOf(context).bottom;
if (keyboard > 120) _stickerPanelHeight = keyboard;
FocusManager.instance.primaryFocus?.unfocus();
_showStickerPanel.value = true;
}
void _onComposerFocusChanged() {
if (_messageFocusNode.hasFocus && _showStickerPanel.value) {
_showStickerPanel.value = false;
}
}
void _onStickerPanelToggle() {
if (_showStickerPanel.value) {
_stickerAnim.forward();
_sendStickerTyping();
_stickerTypingTimer?.cancel();
_stickerTypingTimer = Timer.periodic(
const Duration(seconds: 4),
(_) => _sendStickerTyping(),
);
} else {
_stickerAnim.reverse();
_stickerTypingTimer?.cancel();
_stickerTypingTimer = null;
}
}
void _sendStickerTyping() {
if (KometSettings.ghostMode.value) return;
messagesModule.sendTyping(widget.chatId, 'STICKER');
}
Future<void> _sendSticker(StickerItem sticker) async {
_showStickerPanel.value = false;
await _sendAttachMessage(
[
StickerAttachment(
stickerId: sticker.id.toString(),
baseUrl: sticker.url,
width: sticker.width,
height: sticker.height,
),
],
() => messagesModule.sendStickerMessage(widget.chatId, sticker.id),
);
}
Widget _buildStickerPanel(BuildContext context) {
return AnimatedBuilder(
animation: _stickerAnim,
child: StickerPanel(
height: _stickerPanelHeight,
onStickerTap: _sendSticker,
),
builder: (context, child) {
final t = Curves.easeOutCubic.transform(
_stickerAnim.value.clamp(0.0, 1.0),
);
if (t == 0) return const SizedBox.shrink();
return ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: t,
child: child,
),
);
},
);
}
Future<void> _shareLocation() async { Future<void> _shareLocation() async {
final position = await _resolveCurrentPosition(); final position = await _resolveCurrentPosition();
if (position == null || !mounted) return; if (position == null || !mounted) return;
+390
View File
@@ -0,0 +1,390 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../main.dart' show stickersModule;
import '../../models/sticker.dart';
class _DragScrollBehavior extends MaterialScrollBehavior {
const _DragScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => const {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
};
}
class _Section {
final String title;
final List<int> stickerIds;
final IconData? icon;
final String? iconUrl;
const _Section({
required this.title,
required this.stickerIds,
this.icon,
this.iconUrl,
});
}
class StickerPanel extends StatefulWidget {
final double height;
final void Function(StickerItem sticker) onStickerTap;
const StickerPanel({
super.key,
required this.height,
required this.onStickerTap,
});
@override
State<StickerPanel> createState() => _StickerPanelState();
}
class _StickerPanelState extends State<StickerPanel>
with SingleTickerProviderStateMixin {
static const double _tabBarHeight = 52;
static const double _headerHeight = 34;
final ScrollController _scroll = ScrollController();
late final AnimationController _shimmer;
bool _loading = true;
Object? _error;
int _selectedTab = 0;
List<_Section> _sections = const [];
List<double> _heights = const [];
List<double> _offsets = const [];
@override
void initState() {
super.initState();
_shimmer = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
_scroll.addListener(_onScroll);
_load();
}
@override
void dispose() {
_scroll.removeListener(_onScroll);
_scroll.dispose();
_shimmer.dispose();
super.dispose();
}
Future<void> _load() async {
try {
await stickersModule.ensureLoaded();
if (!mounted) return;
_buildSections();
setState(() => _loading = false);
} catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_error = e;
});
}
}
void _buildSections() {
final sections = <_Section>[];
final recents = stickersModule.recentStickerIds;
if (recents.isNotEmpty) {
sections.add(
_Section(
title: 'Недавние',
stickerIds: recents,
icon: Symbols.schedule,
),
);
}
for (final set in stickersModule.sets) {
if (set.stickerIds.isEmpty) continue;
sections.add(
_Section(
title: set.name,
stickerIds: set.stickerIds,
iconUrl: set.iconUrl,
),
);
}
_sections = sections;
}
void _onScroll() {
if (_offsets.isEmpty) return;
final pixels = _scroll.position.pixels;
var index = 0;
for (var i = 0; i < _offsets.length; i++) {
if (pixels + 1 >= _offsets[i]) index = i;
}
if (index != _selectedTab) setState(() => _selectedTab = index);
}
void _jumpTo(int index) {
if (index < 0 || index >= _offsets.length) return;
setState(() => _selectedTab = index);
final max = _scroll.position.maxScrollExtent;
_scroll.animateTo(
_offsets[index].clamp(0.0, max),
duration: const Duration(milliseconds: 260),
curve: Curves.easeOutCubic,
);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
height: widget.height,
color: cs.surface,
child: _loading
? Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(strokeWidth: 2.4, color: cs.primary),
),
)
: _error != null || _sections.isEmpty
? Center(
child: Text(
_error != null ? 'Не удалось загрузить стикеры' : 'Нет стикеров',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
)
: ScrollConfiguration(
behavior: const _DragScrollBehavior(),
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final columns = (width / 84).floor().clamp(4, 8);
final cell = width / columns;
final heights = <double>[];
final offsets = <double>[];
var acc = 0.0;
for (final s in _sections) {
final rows = (s.stickerIds.length / columns).ceil();
final h = _headerHeight + rows * cell;
offsets.add(acc);
heights.add(h);
acc += h;
}
_heights = heights;
_offsets = offsets;
return Column(
children: [
_buildTabBar(cs),
Divider(
height: 1,
thickness: 1,
color: cs.outlineVariant.withValues(alpha: 0.3),
),
Expanded(
child: ListView.builder(
controller: _scroll,
padding: EdgeInsets.zero,
itemCount: _sections.length,
itemExtentBuilder: (i, _) => _heights[i],
itemBuilder: (context, i) => _StickerSection(
key: ValueKey(_sections[i].title + i.toString()),
title: _sections[i].title,
stickerIds: _sections[i].stickerIds,
columns: columns,
cell: cell,
headerHeight: _headerHeight,
shimmer: _shimmer,
onTap: widget.onStickerTap,
),
),
),
],
);
},
),
),
);
}
Widget _buildTabBar(ColorScheme cs) {
return SizedBox(
height: _tabBarHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 6),
itemCount: _sections.length,
itemBuilder: (context, i) {
final s = _sections[i];
final selected = i == _selectedTab;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _jumpTo(i),
child: Container(
width: 44,
height: 44,
margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
decoration: BoxDecoration(
color: selected ? cs.surfaceContainerHighest : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(8),
child: s.icon != null
? Icon(s.icon, size: 24, color: selected ? cs.primary : cs.onSurfaceVariant)
: CachedNetworkImage(
imageUrl: s.iconUrl ?? '',
fit: BoxFit.contain,
errorWidget: (_, _, _) =>
Icon(Symbols.image, size: 20, color: cs.onSurfaceVariant),
),
),
);
},
),
);
}
}
class _StickerSection extends StatefulWidget {
final String title;
final List<int> stickerIds;
final int columns;
final double cell;
final double headerHeight;
final Animation<double> shimmer;
final void Function(StickerItem sticker) onTap;
const _StickerSection({
super.key,
required this.title,
required this.stickerIds,
required this.columns,
required this.cell,
required this.headerHeight,
required this.shimmer,
required this.onTap,
});
@override
State<_StickerSection> createState() => _StickerSectionState();
}
class _StickerSectionState extends State<_StickerSection> {
bool _loaded = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
await stickersModule.ensureStickers(widget.stickerIds);
if (!mounted) return;
setState(() => _loaded = true);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final ids = widget.stickerIds;
final columns = widget.columns;
final cell = widget.cell;
final rows = (ids.length / columns).ceil();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: widget.headerHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
),
for (var r = 0; r < rows; r++)
Row(
children: [
for (var c = 0; c < columns; c++)
SizedBox(
width: cell,
height: cell,
child: r * columns + c < ids.length
? _cell(ids[r * columns + c])
: null,
),
],
),
],
);
}
Widget _cell(int id) {
if (!_loaded) {
return Padding(
padding: const EdgeInsets.all(6),
child: _ShimmerBox(shimmer: widget.shimmer),
);
}
final item = stickersModule.cachedSticker(id);
if (item == null || item.url.isEmpty) return const SizedBox.shrink();
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => widget.onTap(item),
child: Padding(
padding: const EdgeInsets.all(6),
child: CachedNetworkImage(
imageUrl: item.url,
fit: BoxFit.contain,
memCacheWidth: 220,
fadeInDuration: const Duration(milliseconds: 120),
placeholder: (_, _) => _ShimmerBox(shimmer: widget.shimmer),
errorWidget: (_, _, _) => const SizedBox.shrink(),
),
),
);
}
}
class _ShimmerBox extends StatelessWidget {
final Animation<double> shimmer;
const _ShimmerBox({required this.shimmer});
@override
Widget build(BuildContext context) {
final base = Theme.of(context).colorScheme.surfaceContainerHighest;
return AnimatedBuilder(
animation: shimmer,
builder: (context, _) => DecoratedBox(
decoration: BoxDecoration(
color: base.withValues(alpha: 0.35 + 0.4 * shimmer.value),
borderRadius: BorderRadius.circular(12),
),
child: const SizedBox.expand(),
),
);
}
}
+2
View File
@@ -43,6 +43,7 @@ import 'backend/modules/file_uploader.dart';
import 'backend/modules/messages.dart'; import 'backend/modules/messages.dart';
import 'backend/modules/outbox.dart'; import 'backend/modules/outbox.dart';
import 'backend/modules/polls.dart'; import 'backend/modules/polls.dart';
import 'backend/modules/stickers.dart';
import 'backend/modules/self_check.dart'; import 'backend/modules/self_check.dart';
import 'backend/modules/webapp.dart'; import 'backend/modules/webapp.dart';
import 'backend/modules/digital_id.dart'; import 'backend/modules/digital_id.dart';
@@ -69,6 +70,7 @@ final api = Api();
final accountModule = AccountModule(api); final accountModule = AccountModule(api);
final messagesModule = MessagesModule(api); final messagesModule = MessagesModule(api);
final pollsModule = PollsModule(api); final pollsModule = PollsModule(api);
final stickersModule = StickersModule(api);
final webAppModule = WebAppModule(api); final webAppModule = WebAppModule(api);
final digitalIdModule = DigitalIdModule(webAppModule); final digitalIdModule = DigitalIdModule(webAppModule);
final fileUploader = FileUploader(api: api, messages: messagesModule); final fileUploader = FileUploader(api: api, messages: messagesModule);
+56
View File
@@ -0,0 +1,56 @@
class StickerSet {
final int id;
final String name;
final String iconUrl;
final List<int> stickerIds;
final String? link;
const StickerSet({
required this.id,
required this.name,
required this.iconUrl,
required this.stickerIds,
this.link,
});
factory StickerSet.fromMap(Map<dynamic, dynamic> map) {
final rawStickers = map['stickers'];
final ids = <int>[];
if (rawStickers is List) {
for (final e in rawStickers) {
if (e is int) ids.add(e);
}
}
return StickerSet(
id: map['id'] as int,
name: map['name']?.toString() ?? '',
iconUrl: map['iconUrl']?.toString() ?? '',
stickerIds: ids,
link: map['link']?.toString(),
);
}
}
class StickerItem {
final int id;
final String url;
final int? setId;
final int? width;
final int? height;
const StickerItem({
required this.id,
required this.url,
this.setId,
this.width,
this.height,
});
factory StickerItem.fromMap(Map<dynamic, dynamic> map) => StickerItem(
id: map['id'] as int,
url: map['url']?.toString() ?? '',
setId: map['setId'] as int?,
width: map['width'] as int?,
height: map['height'] as int?,
);
}