Files
Qlyra/lib/backend/modules/stickers.dart
T
2026-07-05 20:50:26 +07:00

285 lines
7.8 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) {
_api.pushStream
.where((p) => p.opcode == Opcode.notifAssetsUpdate)
.listen(_handleAssetsPush);
}
final Map<int, StickerSet> _sets = {};
final Map<int, StickerItem> _stickers = {};
List<int> _orderedSetIds = [];
List<int> _favoriteSetIds = [];
List<int> _recentStickerIds = [];
Future<void>? _loading;
Future<void>? _favoritesLoading;
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];
StickerSet? cachedSet(int id) => _sets[id];
bool isFavorite(int setId) => _favoriteSetIds.contains(setId);
Future<void> ensureLoaded() {
return _loading ??= _loadSections().catchError((Object e) {
_loading = null;
throw e;
});
}
Future<void> ensureFavoritesLoaded() {
return _favoritesLoading ??= _loadFavorites().catchError((Object e) {
_favoritesLoading = null;
throw e;
});
}
Future<void> _loadFavorites() async {
final favIds = <int>[];
final fav = await _api.sendRequestMap(Opcode.assetsUpdate, {
'type': 'FAVORITE_STICKER',
'sync': 0,
});
if (fav != null) {
final sections = fav['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;
final stickerData = await _api.sendRequestMap(Opcode.assetsUpdate, {
'type': 'STICKER',
'sync': 0,
});
if (stickerData != null) {
final sections = stickerData['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.sendRequestMap(Opcode.assetsGet, {
'sectionId': 'NEW_STICKER_SETS',
'from': marker,
'count': 100,
});
if (page == null) break;
final before = newSetIds.length;
_appendIntList(newSetIds, page['stickerSets']);
if (newSetIds.length == before) break;
final m = page['marker'];
marker = m is int ? m : 0;
}
await ensureFavoritesLoaded();
final ordered = <int>[];
final seen = <int>{};
for (final id in [..._favoriteSetIds, ...newSetIds]) {
if (seen.add(id)) ordered.add(id);
}
_orderedSetIds = ordered;
logger.i(
'Стикеры: ${ordered.length} паков, ${_recentStickerIds.length} недавних',
);
await _ensureSetMetas(ordered);
}
Future<void> _fetchAndCache<T>({
required String type,
required List<int> ids,
required String listKey,
required T Function(Map) fromMap,
required Map<int, T> cache,
}) async {
final missing = ids.where((id) => !cache.containsKey(id)).toList();
for (final batch in _chunk(missing, 100)) {
final map = await _api.sendRequestMap(Opcode.assetsGetByIds, {
'type': type,
'ids': batch,
});
if (map == null) continue;
final list = map[listKey];
if (list is! List) continue;
for (final e in list) {
if (e is Map && e['id'] is int) {
cache[e['id'] as int] = fromMap(e);
}
}
}
}
Future<void> _ensureSetMetas(List<int> ids) => _fetchAndCache<StickerSet>(
type: 'STICKER_SET',
ids: ids,
listKey: 'stickerSets',
fromMap: StickerSet.fromMap,
cache: _sets,
);
Future<List<StickerItem>> ensureStickers(List<int> stickerIds) async {
await _fetchAndCache<StickerItem>(
type: 'STICKER',
ids: stickerIds,
listKey: 'stickers',
fromMap: StickerItem.fromMap,
cache: _stickers,
);
return stickerIds
.map((id) => _stickers[id])
.whereType<StickerItem>()
.toList();
}
Future<StickerSet?> ensureSet(int setId) async {
await _ensureSetMetas([setId]);
return _sets[setId];
}
Future<void> ensureAllStickersLoaded() {
final ids = <int>{..._recentStickerIds};
for (final set in _sets.values) {
ids.addAll(set.stickerIds);
}
return ensureStickers(ids.toList());
}
List<StickerItem> searchByTags(Set<String> emojiTargets) {
if (emojiTargets.isEmpty) return const [];
final result = <StickerItem>[];
for (final sticker in _stickers.values) {
for (final tag in sticker.tags) {
if (emojiTargets.contains(_stripVariation(tag))) {
result.add(sticker);
break;
}
}
}
return result;
}
static String _stripVariation(String s) => s.replaceAll('', '');
void cacheSet(StickerSet set) => _sets[set.id] = set;
Future<StickerSet?> resolveSetByLink(String link) async {
final map = await _api.sendRequestMap(Opcode.linkInfo, {'link': link});
if (map == null) return null;
final raw = map['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 map = await _api.sendRequestMap(Opcode.assetsAdd, {
'type': 'FAVORITE_STICKER_SET',
'id': setId,
});
final ok = map != null && map['success'] == true;
if (ok) _markFavorite(setId, true);
return ok;
}
Future<bool> unfavoriteSet(int setId) async {
final map = await _api.sendRequestMap(Opcode.assetsRemove, {
'type': 'FAVORITE_STICKER_SET',
'ids': [setId],
});
final ok = map != null && map['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>[];
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);
}
}
}