import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'dart:typed_data'; import 'package:image/image.dart'; import 'package:injectable/injectable.dart'; import 'package:isar/isar.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:archive/archive.dart'; import 'package:mnemo_cards_backend/packs/card_dto_extension.dart'; import 'package:mnemo_cards_backend/packs/card_pack_model_extension.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import '../main.dart'; import 'pack_dto_converter.dart'; @lazySingleton class PackManager { final PackDtoConverter packDtoConverter; PackManager(this.packDtoConverter); Future> listPacksPreviews( UserModel? userModel, Map? params, ) async { final models = await isar.cardPackModels .filter() .optional( userModel?.admin != true, (q) => q.enabledEqualTo(true), ) .findAll(); models.sort((p, n) => p.order.compareTo(n.order)); return (await Future.wait(models.map( (model) async { final dto = await packDtoConverter.toCardPackPreviewDto(model, userModel); if (!model.enabled) { return dto.copyWith(subtitle: 'DISABLED ${dto.subtitle}'); } return dto; }, ))) ..sort((prev, next) { final bothAvailable = prev.isAvailable && next.isAvailable; if (bothAvailable) { return 0; } final prevAd = canOpenForAd(int.tryParse(prev.id)); final nextAd = canOpenForAd(int.tryParse(next.id)); final orderAds = prevAd == nextAd ? 0 : prevAd ? -1 : 1; final orderAvailability = prev.isAvailable == next.isAvailable ? 0 : prev.isAvailable ? -1 : 1; return orderAvailability == 0 ? orderAds : orderAvailability; }); } //no images Future> listPacksActions( Map? packsData, UserModel user, ) async { final availablePacks = await isar.txn(() async { await user.subscriptionModel.load(); if (user.subscriptionModel.value?.features .contains(SubscriptionFeatureEnum.packs) == true) { return isar.cardPackModels.filter().enabledEqualTo(true).findAll(); } else { return user.packs.filter().enabledEqualTo(true).findAll(); } }); List response = []; for (final pack in availablePacks) { final expectedUserVersion = pack.version; final userPackVersion = packsData?[pack.id.toString()]; if (userPackVersion == expectedUserVersion) { //last version continue; } else { // needs update response.add( CardPackAction( id: pack.id.toString(), action: PackAction.update, ), ); } } //if front has packs we dont have - action delete for (final id in packsData!.keys.where( (id) => id.isNotEmpty && availablePacks.where((p) => p.id.toString() == id.toString()).isEmpty, )) { response.add( CardPackAction( id: id, action: PackAction.delete, ), ); } return response; } // Future addPack(CardPackModel model, String productId) async { // await isar.writeTxn( // () async { // if (model.id != null && model.id! < 0) { // model = model.copyWith(id: null); // } // await isar.cardPackModels.put(model); // final cards = cardPackDto.cards.map((e) => e.toModel()).toList(); // isar.gameCardModels.putAll(cards); // await model.cards // ..addAll(cards) // ..save(); // }, // ); // } Future deletePack(String idString) async { return isar.writeTxn(() async { final id = int.tryParse(idString); if (id == null) return false; final model = await isar.cardPackModels.get(id); if (model == null) return false; return await isar.cardPackModels.delete(id); }); } Future deleteCard(String idString) async { return isar.writeTxn(() async { final id = int.tryParse(idString); if (id == null) return false; final model = await isar.gameCardModels.get(id); if (model == null) return false; await model.packs.load(); if (model.packs.isNotEmpty) return false; return await isar.gameCardModels.delete(id); }); } Future addCard(GameCardDto dto) async { await isar.writeTxn(() async { GameCardModel model; if (dto.id < 0) { print('Creating new card ${dto.original}'); model = dto.toModel().copyWith(id: null); } else { final existingModel = await isar.gameCardModels.get(dto.id); if (existingModel != null) { print('Editing card ${dto.id} ${dto.original}'); model = existingModel.copyWith( image: dto.image ?? existingModel.image, mnemo: dto.mnemo ?? existingModel.mnemo, original: dto.original ?? existingModel.original, translation: dto.translation ?? existingModel.translation, transcription: dto.transcription ?? existingModel.transcription, imageBack: dto.imageBack ?? existingModel.imageBack, transcriptionMnemo: dto.transcriptionMnemo ?? existingModel.transcriptionMnemo, ); } else { print('Creating new card with id ${dto.id} ${dto.original}'); model = dto.toModel(); } } final cardId = await isar.gameCardModels.put(model); if (model.image.length > 100) { print('Adding base64 image for card ${dto.id} ${dto.original}'); //base64 image try { final data = base64Decode(model.image); final image = decodeImage(data)!; final fileName = '${cardId}_${dto.original}.png'; final resizedImage = copyResize(image, width: 1024, height: 1024); final png = encodePng(resizedImage, level: 9); final imageFile = File('${assetsDirectory.path}/cards/$fileName'); for (final imageSize in _ImageSize.values) { final resizedFile = _getResizedFile(fileName, 'cards', imageSize); try { if (await resizedFile.exists()) { print( 'Removing old image cache for ${imageSize.name}: ${resizedFile.path}'); await resizedFile.delete(); } } catch (_) { print('Error deleting resized image ${resizedFile.path}'); } } imageFile.writeAsBytesSync(png, flush: true); print('Card image saved ${imageFile.path}'); model = model.copyWith(image: fileName); } catch (_) { throw Exception('Some exception'); } } final id = await isar.gameCardModels.put(model); final cardPackModel = (await isar.gameCardModels.get(id))!; await cardPackModel.packs.load(); final packs = cardPackModel.packs.toList(); for (final pack in packs) { await isar.cardPackModels.put( pack.copyWith(version: pack.version.incVersion), ); } }); } Future getPackOrBuy( String packId, UserModel? userModel, ) async { final model = await _fetchPackModel(packId); if (userModel?.packs.contains(packId) != true) { return packDtoConverter.toCardPackBuyDto(model, userModel); } return packDtoConverter.toDto(model); } Future getPackModelIfAvailable( UserModel userModel, String packId, ) async { final id = int.parse(packId); userModel.subscriptionModel.load(); final allPacksAvailable = userModel.subscriptionModel.value?.features.contains( SubscriptionFeatureEnum.packs, ) == true; if (allPacksAvailable) { final model = await isar.txn(() => isar.cardPackModels.get(id)); if (model?.enabled == true) { return model; } return null; } final model = await userModel.packs .filter() .enabledEqualTo(true) .idEqualTo(id) .findFirst(); return model; } Future getPack( String packId, UserModel? userModel, ) async { // Preview pack (id=10) доступен без авторизации if (packId == '10') { final model = await _fetchPackModel(packId); if (model.enabled) { return packDtoConverter.toDto(model); } return null; } if (userModel == null) { return null; } final model = await getPackModelIfAvailable(userModel, packId); if (model == null) { return null; } return packDtoConverter.toDto(model); } Future getEditPack(String packId) async { final id = int.tryParse(packId); if (id == null) return null; return isar.txn(() async { final model = await isar.cardPackModels.get(id); if (model == null) return null; return await packDtoConverter.toEditDto(model); }); } Future editPack(EditCardPackDto dto) async { return isar.writeTxn(() async { final oldId = int.tryParse(dto.id ?? ''); final oldModel = oldId == null ? null : await isar.cardPackModels.get(oldId); final editModel = packDtoConverter.toModel(dto, oldModel); final dtoPreviewCardsIds = dto.previewCards?.map(int.tryParse).whereNotNull().toList() ?? []; final dtoAddCardsIds = dto.addCardIds?.map(int.tryParse).whereNotNull().toList() ?? []; final dtoAddTestIds = dto.addTestIds?.map(int.tryParse).whereNotNull().toList() ?? []; final dtoPreviewCards = dtoPreviewCardsIds.isEmpty ? [] : (await isar.gameCardModels.getAll(dtoPreviewCardsIds)) .whereNotNull() .toList(); final dtoAddCards = dtoAddCardsIds.isEmpty ? [] : (await isar.gameCardModels.getAll(dtoAddCardsIds)) .whereNotNull() .toList(); final dtoAddTests = dtoAddTestIds.isEmpty ? [] : (await isar.testModels.getAll(dtoAddTestIds)) .whereNotNull() .toList(); final modelId = await isar.cardPackModels.put( editModel.copyWith.version(editModel.version.incVersion), ); final savedEditModel = (await isar.cardPackModels.get(modelId))!; if (dtoPreviewCards.isNotEmpty) { await savedEditModel.previewCards.reset(); savedEditModel.previewCards.addAll(dtoPreviewCards); await savedEditModel.previewCards.save(); } await savedEditModel.cards.load(); savedEditModel.cards ..removeWhere( (card) => dto.removeCardIds?.contains(card.id.toString()) ?? false, ) ..addAll(dtoAddCards); await savedEditModel.cards.save(); await savedEditModel.tests.load(); savedEditModel.tests ..removeWhere( (test) => dto.removeTestIds?.contains(test.id.toString()) ?? false, ) ..addAll(dtoAddTests); await savedEditModel.tests.save(); return true; }); } Future getBuyPage(String packId, UserModel? user) async { final id = int.tryParse(packId); if (id == null) { return null; } if (await user?.packs.filter().idEqualTo(id).findFirst() == null) { final model = await _fetchPackModel(packId); return packDtoConverter.toCardPackBuyDto(model, user); } return null; } Future getPublicBuyPage(String packId) async { final id = int.tryParse(packId); if (id == null) { return null; } final model = await isar.cardPackModels.get(id); if (model == null || !model.enabled) { return null; } return packDtoConverter.toCardPackBuyDto(model, null); } Future _fetchPackModel(String packId) async { return (await isar.cardPackModels.get(int.parse(packId)))!; } Future> fetchPackImagesArchive( String packId, int userId, String appVersion, ) async { final pack = await _fetchPackModel(packId); try { final archiveFile = File( '${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip', ); if (await archiveFile.exists()) { return await archiveFile.readAsBytes(); } } catch (e, s) { log('Error when fetching pack images archive', error: e, stackTrace: s); return []; } final cards = pack.cards; final rawData = _fetchPackImages(cards.toList()); final archive = Archive(); for (final entry in rawData.entries) { archive.addFile(ArchiveFile(entry.key, 0, entry.value)); } final password = TokenGenerator.generateArchivePassword( appVersion: appVersion, id: packId, ); final bytes = ZipEncoder( password: password, ).encode( archive, level: Deflate.BEST_COMPRESSION, )!; Future saveFile() async { try { await archiveFile.create(recursive: true); await archiveFile.writeAsBytes(bytes); } catch (e, s) { log( 'Error when creating archive ${archiveFile.path}', error: e, stackTrace: s, ); } } unawaited(saveFile()); return bytes; } Map> _fetchPackImages(List cards) { return Map.fromEntries( cards.map((e) { final empty = MapEntry(e.id.toString(), []); try { final image = e.image.isEmpty ? empty : MapEntry( e.id.toString(), File('${assetsDirectory.path}/cards/${e.image}') .readAsBytesSync(), ); return image; } catch (e, s) { log('error while reading image', error: e, stackTrace: s); return empty; } }), ); } static Directory get assetsDirectory { String mainPath = Platform.resolvedExecutable; if ((Platform.isMacOS || Platform.isLinux) && !Platform.script.toString().contains('StudioProjects')) { mainPath = mainPath.substring(0, mainPath.lastIndexOf("/")); var dir = Directory("$mainPath/../data"); if (dir.existsSync()) { return dir; } dir = Directory("$mainPath/data"); if (dir.existsSync()) { return dir; } throw Exception('No asset dir! $mainPath'); } if (Platform.isMacOS) { mainPath = mainPath.substring(0, mainPath.lastIndexOf("/")); return Directory( '/Users/dmitry/StudioProjects/mnemo_cards/mnemo_cards_backend/data'); } else if (Platform.isWindows) { mainPath = mainPath.substring(0, mainPath.lastIndexOf("\\")); return Directory("$mainPath/data/flutter_assets/data"); } else { return Directory(''); } } static File _getResizedFile(String id, String type, _ImageSize size) => File('${PackManager.assetsDirectory.path}/$type/${size.name}/$id'); } enum _ImageSize { big, medium, small, extraSmall, } extension on _ImageSize { int get width => switch (this) { _ImageSize.big => 1024, _ImageSize.medium => 512, _ImageSize.small => 320, _ImageSize.extraSmall => 192, }; } extension coverStringExt on String { Future get base64Image => _base64Image(); Future _base64FromBytes(Uint8List bytes) async => base64.normalize(base64Encode(bytes)); // base64 or cardId or 'cards/id' or 'images/id' Future _base64Image([_ImageSize? size]) async { try { if (length > 100) { if (size != null) { final bytes = base64Decode(this); final image = decodeImage(bytes)!; final reizedImage = copyResize(image, width: size.width); return _base64FromBytes(encodePng(reizedImage)); } //is base 64 return this; } if (!contains('/')) { return _base64FromBytes(_getById(this, 'cards', size)); } else if (startsWith('cards/')) { return _base64FromBytes(_getById(split('/').last, 'cards', size)); } else if (startsWith('images/')) { return _base64FromBytes(_getById(split('/').last, 'images', size)); } } catch (e, s) { log(e.toString(), stackTrace: s); } return ''; } Uint8List _getById(String id, String type, _ImageSize? size) { if (size == null) { return File('${PackManager.assetsDirectory.path}/$type/$id') .readAsBytesSync(); } final resizedFile = PackManager._getResizedFile(id, type, size); if (resizedFile.existsSync()) { try { return resizedFile.readAsBytesSync(); } catch (_) { print( 'Error, deleting ${size.name} image with id:$id,type:$type (${resizedFile.path})'); resizedFile.deleteSync(); } } else { print( 'No ${size.name} image with id:$id,type:$type (${resizedFile.path})'); } final file = File('${PackManager.assetsDirectory.path}/$type/$id'); if (file.existsSync()) { try { final bytes = file.readAsBytesSync(); final image = decodeImage(bytes)!; final reizedImage = copyResize(image, width: size.width); final resizedBytes = encodePng(reizedImage); resizedFile.createSync(recursive: true); resizedFile.writeAsBytesSync(resizedBytes); print( 'Saved ${size.name} image with id:$id,type:$type (${resizedFile.path})'); return resizedBytes; } catch (e, s) { log('error while resizing image', error: e, stackTrace: s); } } return Uint8List(0); } Future get smallBase64Image => _base64Image(_ImageSize.small); Future get extraSmallBase64Image => _base64Image(_ImageSize.extraSmall); Future get mediumBase64Image => _base64Image(_ImageSize.medium); Future get bigSmallBase64Image => _base64Image(null); }