import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; import 'package:archive/archive_io.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:mnemo_cards/features/packs/loading_packs_holder.dart'; import 'package:mnemo_cards/features/packs/preview_packs_poller.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rxdart/rxdart.dart'; import '../analytics/analytics.dart'; import 'pack_cache_manager.dart'; import 'pack_holder.dart'; import 'packs_api.dart'; import 'preview_pack_holder.dart'; /// Updates cache and state holders for available packs /// Does polling class PackUpdater { final PacksApi _packsApi; final PackCacheManager _cacheManager; final PackHolder _packHolder; final PreviewPackHolder _previewPackHolder; final LoadingPacksHolder _loadingPacksHolder; final PreviewPacksPoller _previewPacksPoller; Completer? _updateCompleter; PackUpdater( this._packsApi, this._cacheManager, this._packHolder, this._previewPackHolder, this._loadingPacksHolder, this._previewPacksPoller, ); Future init() async { final pollStream = Stream.periodic(const Duration(seconds: 600)); pollStream.startWith(0).listen((event) async { await updateAvailablePacks(); await _previewPacksPoller.poll(); }); } Future updateAvailablePacks() async { if (_updateCompleter?.isCompleted ?? true) { _updateCompleter = Completer(); } try { print('Updating packs started'); await _updateAvailablePacks(); } catch (e, s) { log('Error when updating packs', error: e, stackTrace: s); print('Error when updating pack: ${e}'); // Future.delayed(Duration(seconds: 5), () => updateAvailablePacks()); } finally { print('Updating packs complete'); if (_updateCompleter?.isCompleted == false) { _updateCompleter?.complete(); } } return _updateCompleter!.future; } Future _updateAvailablePacks() async { final savedList = await _cacheManager.loadAllPacks(); final packData = Map.fromEntries( savedList.map( (e) => MapEntry( e.id.toString(), e.version, ), ), ); final actions = await _packsApi.getPacksActions(packData); Map updatedPacks = {}; bool shouldUpdateHolder = false; for (final action in actions) { switch (action.action) { case PackAction.delete: log('DELETE ${action.id}'); shouldUpdateHolder = true; await _cacheManager.deletePack(action.id); savedList.removeWhere((p) => p.id == action.id); break; case PackAction.update: log('UPDATE ${action.id}'); shouldUpdateHolder = true; try { final updatedPack = await _updatePackAndSaveImages(action.id); updatedPacks[action.id] = updatedPack; } catch (e) { log('Error while download cards for ${action.id} $e'); } break; case PackAction.unknown: log('Unknown action on update ${action.id}'); break; } } if (shouldUpdateHolder) { List packs = []; for (final saved in savedList) { if (updatedPacks[saved.id] != null) { packs.add(updatedPacks[saved.id]!); updatedPacks.remove(saved.id); } else { packs.add(saved); } } packs.addAll(updatedPacks.values); _packHolder.setPacksWithCards(packs.map((p) => p.id).toList()); } } Future updatePackAndSaveImages( String packId, { bool shouldUpdateHolder = false, }) { _previewPacksPoller.poll(); return _updatePackAndSaveImages( packId, shouldUpdateHolder: shouldUpdateHolder, ); } Future _updatePackAndSaveImages( String packId, { bool shouldUpdateHolder = false, }) async { final loadingDto = _previewPackHolder.state?.previewPacks .firstWhereOrNull((p) => p.id == packId) ?.let(CardPackLoadingDto.fromPreviewDto) ?? CardPackLoadingDto.fromId(packId, message: 'Загружаем карточки...'); try { _loadingPacksHolder.addLoadingPack(loadingDto); log('Updating with cards ${packId}', name: 'updatePackWithCards'); final s = Stopwatch()..start(); final packDto = await _packsApi.getPackDto(packId); print('get from api ${s.elapsedMilliseconds} mills'); if (packDto == null) { throw Exception('Cant update pack $packId'); } final imagesData = await _packsApi.packImagesArchive(packId); print('get cards from api ${s.elapsedMilliseconds} mills'); _loadingPacksHolder.addLoadingPack( loadingDto.copyWith(message: 'Распаковываем карточки...'), ); final appVersion = (await PackageInfo.fromPlatform()).version; final images = await compute( _decodeArchive, _Envelope( data: imagesData, packId: packId, appVersion: appVersion, ), ); _loadingPacksHolder.addLoadingPack( loadingDto.copyWith(message: 'Почти всё'), ); await _cacheManager.savePack(packDto, images: images); print('saved pack ${s.elapsedMilliseconds} mills'); log('Pack ${packId} updated with cards'); if (shouldUpdateHolder) { _packHolder.setPacksWithCards( [ ..._packHolder.state.packsWithCards, packDto.id, ], ); } return packDto; } catch (e, s) { log('Cant update pack', error: e, stackTrace: s); Analytics.crashlyticsError( message: 'Cant update pack', e: e, s: s, ); rethrow; } finally { _loadingPacksHolder.removeLoadingPack(packId); } } } Map> _decodeArchive(_Envelope envelope) { final s = Stopwatch()..start(); final password = TokenGenerator.generateArchivePassword( appVersion: envelope.appVersion, packId: envelope.packId, ); print('generate token ${s.elapsedMilliseconds} mills'); final imagesArchive = ZipDecoder().decodeBytes( envelope.data, password: password, ); print('deflate archive ${s.elapsedMilliseconds} mills'); final images = Map>(); for (final file in imagesArchive) { images[file.name] = file.content as List; } print('loaded images ${s.elapsedMilliseconds} mills'); return images; } class _Envelope { final List data; final String packId; final String appVersion; _Envelope({ required this.data, required this.packId, required this.appVersion, }); }