575 lines
18 KiB
Dart
575 lines
18 KiB
Dart
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<List<CardPackPreviewDto>> listPacksPreviews(
|
|
UserModel? userModel,
|
|
Map<String, String>? 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<List<CardPackAction>> listPacksActions(
|
|
Map<String, String?>? 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<CardPackAction> 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<void> 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<bool> 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<bool> 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<void> 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<GetCardPackResponse?> 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<CardPackModel?> 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<GetCardPackResponse?> 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<EditCardPackDto?> 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<bool> 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
|
|
? <GameCardModel>[]
|
|
: (await isar.gameCardModels.getAll(dtoPreviewCardsIds))
|
|
.whereNotNull()
|
|
.toList();
|
|
final dtoAddCards = dtoAddCardsIds.isEmpty
|
|
? <GameCardModel>[]
|
|
: (await isar.gameCardModels.getAll(dtoAddCardsIds))
|
|
.whereNotNull()
|
|
.toList();
|
|
final dtoAddTests = dtoAddTestIds.isEmpty
|
|
? <TestModel>[]
|
|
: (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<CardPackBuyDto?> 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<CardPackBuyDto?> 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<CardPackModel> _fetchPackModel(String packId) async {
|
|
return (await isar.cardPackModels.get(int.parse(packId)))!;
|
|
}
|
|
|
|
Future<List<int>> fetchPackImagesArchive(
|
|
String packId,
|
|
int userId,
|
|
String appVersion,
|
|
) async {
|
|
final pack = await _fetchPackModel(packId);
|
|
final archiveFile = File(
|
|
'${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip',
|
|
);
|
|
if (await archiveFile.exists()) {
|
|
return archiveFile.readAsBytes();
|
|
}
|
|
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<void> 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<String, List<int>> _fetchPackImages(List<GameCardModel> cards) {
|
|
return Map.fromEntries(
|
|
cards.map((e) {
|
|
final empty = MapEntry(e.id.toString(), <int>[]);
|
|
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<String> get base64Image => _base64Image();
|
|
|
|
Future<String> _base64FromBytes(Uint8List bytes) async =>
|
|
base64.normalize(base64Encode(bytes));
|
|
|
|
// base64 or cardId or 'cards/id' or 'images/id'
|
|
Future<String> _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<String> get smallBase64Image => _base64Image(_ImageSize.small);
|
|
|
|
Future<String> get extraSmallBase64Image =>
|
|
_base64Image(_ImageSize.extraSmall);
|
|
|
|
Future<String> get mediumBase64Image => _base64Image(_ImageSize.medium);
|
|
|
|
Future<String> get bigSmallBase64Image => _base64Image(null);
|
|
}
|