This commit is contained in:
Dmitry 2025-12-19 04:09:04 +03:00
parent 7aa12d0c59
commit ce9614f676
9 changed files with 21 additions and 250 deletions

View file

@ -1,7 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart' as d;
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/acl_types.dart';
import 'package:mnemo_cards_backend/api/authorize/access_service.dart';
@ -10,7 +8,7 @@ import 'package:mnemo_cards_backend/database/database.dart' hide VoiceModel;
import 'package:mnemo_cards_backend/database/database.dart' as drift show VoiceModel;
import 'package:mnemo_cards_backend/packs/card_pack_drift_extension.dart';
import 'package:mnemo_cards_backend/packs/card_image_storage.dart';
import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager, PackManagerUtils;
import 'package:mnemo_cards_backend/packs/pack_manager.dart' show PackManager;
import 'package:mnemo_cards_backend/packs/voice_storage.dart';
import 'package:mnemo_cards_backend/storage/minio_config.dart';
import 'package:mnemo_cards_backend/storage/minio_service.dart';
@ -511,25 +509,8 @@ class PacksApiV2 {
return Response.found(imageValue);
}
// Legacy: try to resolve from local file system
final resolved = await CardImageStorage.tryResolveLocalFile(
cardId: cardId,
imageValue: imageValue,
isBack: false,
);
if (resolved == null) {
print('Could not resolve image file for card $cardId');
return _notFound('Image not found');
}
return Response.ok(
resolved.bytes,
headers: {
'Content-Type': resolved.contentType,
'Cache-Control': 'public, max-age=86400', // Cache for 1 day
},
);
// Not a UUID and not a remote URL - image not found
return _notFound('Image not found');
} catch (e, s) {
print('Error fetching card image: $e\n$s');
return _internalServerError('Error loading image');
@ -601,57 +582,8 @@ class PacksApiV2 {
return Response.found(imageValue);
}
// Legacy: try to resolve from local file system
final resolved = await CardImageStorage.tryResolveLocalFile(
cardId: cardId,
imageValue: imageValue,
isBack: true,
);
if (resolved != null) {
if (resolved.fileName != card.imageBack) {
await _db.packDao.updateCard(
card.copyWith(imageBack: d.Value(resolved.fileName)),
);
}
return Response.ok(
resolved.bytes,
headers: {
'Content-Type': resolved.contentType,
'Cache-Control': 'public, max-age=86400', // Cache for 1 day
},
);
}
final stored = await CardImageStorage.persistFromBase64(
cardId: cardId,
imageValue: imageValue,
preferredFileName: null,
isBack: true,
);
if (stored == null) {
return _notFound('Back image not found');
}
await _db.packDao.updateCard(
card.copyWith(imageBack: d.Value(stored.fileName)),
);
final file = File('${PackManagerUtils.assetsDirectory.path}/cards/${stored.fileName}');
if (!file.existsSync()) {
return _notFound('Back image not found');
}
final bytes = await file.readAsBytes();
return Response.ok(
bytes,
headers: {
'Content-Type': stored.contentType,
'Cache-Control': 'public, max-age=86400', // Cache for 1 day
},
);
// Not a UUID and not a remote URL - image not found
return _notFound('Back image not found');
} catch (e, s) {
print('Error fetching card back image: $e\n$s');
return _internalServerError('Error loading back image');
@ -826,42 +758,7 @@ class PacksApiV2 {
return _notFound('Voice file not found in storage');
}
// Legacy: local voice file
final resolved = await VoiceStorage.tryResolveLocalFile(
voiceValue: voiceValue,
);
if (resolved != null) {
return Response.ok(
resolved.bytes,
headers: {
'Content-Type': resolved.contentType,
'Cache-Control': 'public, max-age=86400',
},
);
}
// Legacy base64 stored in DB: migrate to file and serve bytes.
final stored = await VoiceStorage.persistFromBase64(
voiceId: voice.id,
voiceValue: voiceValue,
);
if (stored != null) {
await _db.packDao.updateVoiceUrl(voice.id, stored.fileName);
final migratedResolved = await VoiceStorage.tryResolveLocalFile(
voiceValue: stored.fileName,
);
if (migratedResolved != null) {
return Response.ok(
migratedResolved.bytes,
headers: {
'Content-Type': migratedResolved.contentType,
'Cache-Control': 'public, max-age=86400',
},
);
}
}
// Not a UUID and not a remote URL - voice file not found
return _notFound('Voice file not found');
} catch (e, s) {
print('Error fetching voice file: $e\n$s');

View file

@ -3,7 +3,6 @@ import 'package:mnemo_cards_backend/api/ads/ads_manager.dart';
import 'package:mnemo_cards_backend/extensions.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:mnemo_cards_backend/packs/card_model_extension.dart';
import 'package:mnemo_cards_backend/packs/pack_manager_extensions.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'products_price_resolver.dart';
@ -45,9 +44,7 @@ class PackDtoConverter {
cards: model.cards.length,
tests: model.tests.length,
price: price,
imageBase64: model.cover != null
? await model.cover!.smallBase64Image
: null,
imageBase64: null, // Legacy: covers should be stored in MinIO, not read from disk
color: model.color,
isAvailable: available,
// trail: 'asset:icons/gift.png',
@ -95,9 +92,7 @@ class PackDtoConverter {
.toDtosList(model.cardsOrder)
.map(
(dto) async => dto.copyWith(
image: dto.image != null
? await dto.image!.smallBase64Image
: null,
image: null, // Legacy: card images should be stored in MinIO, not read from disk
),
),
),
@ -158,7 +153,7 @@ class PackDtoConverter {
subtitle: model.subtitle,
color: model.color,
version: model.version,
cover: model.cover != null ? await model.cover!.base64Image : null,
cover: null, // Legacy: covers should be stored in MinIO, not read from disk
size: model.size,
googlePlayId: model.googlePlayId,
rustoreId: model.rustoreId,
@ -173,7 +168,7 @@ class PackDtoConverter {
removeCardIds: null,
removeTestIds: null,
previewCards: model.previewCards
.map((card) => card.id?.toString())
.map((card) => card.id.toString())
.whereNotNull()
.toList(),
order: model.order,

View file

@ -1,14 +1,11 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart' hide VoiceModel;
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'pack_dto_converter.dart';
import 'pack_manager_extensions.dart';
import 'card_pack_drift_extension.dart';
@lazySingleton
@ -76,53 +73,6 @@ class PackManager {
return await pack.toDto(cards, voices, userModel);
}
Future<List<String>> getPackPreviewImages(String packId) async {
final pack = await getPack(packId);
if (pack == null) {
throw StateError('Pack not found');
}
final previewCards = await _db.packDao.getPreviewCards(packId);
final cards = previewCards.isNotEmpty ? previewCards : (await getCards(packId)).take(6).toList();
final images = <String>[];
for (final card in previewCards) {
if (card.image.isNotEmpty) {
try {
final image = await card.image.base64Image;
images.add(image);
} catch (e, s) {
log('error while reading image', error: e, stackTrace: s);
}
}
}
// If we don't have enough preview images, fill with empty strings
while (images.length < 6) {
images.add('');
}
return images;
}
Future<Map<String, Uint8List>> getPackImages(String packId) async {
final cards = await getCards(packId);
final result = <String, Uint8List>{};
for (final card in cards) {
if (card.image.isNotEmpty) {
try {
result[card.id.toString()] =
File('${PackManagerUtils.assetsDirectory.path}/cards/${card.image}')
.readAsBytesSync();
} catch (e, s) {
log('error while reading image', error: e, stackTrace: s);
}
}
}
return result;
}
}
// Static utility functions for PackManager

View file

@ -1,56 +0,0 @@
// Extensions and utilities for PackManager
import 'dart:typed_data';
import 'dart:io';
import 'dart:convert';
import 'package:image/image.dart';
import 'pack_manager.dart';
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> _base64Image([_ImageSize? size]) async {
if (this == null) return '';
try {
final bytes = _getById(this!, 'covers', size);
return base64Encode(bytes);
} catch (e) {
return '';
}
}
Uint8List _getById(String id, String type, _ImageSize? size) {
try {
return File('${PackManagerUtils.assetsDirectory.path}/$type/$id')
.readAsBytesSync();
} catch (e) {
print('Error loading image $id of type $type: $e');
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 bigBase64Image => _base64Image(null);
}

View file

@ -27,7 +27,7 @@ class MinioConfig {
factory MinioConfig.fromEnvironment() {
return MinioConfig(
endpoint: const String.fromEnvironment(
'MINIO_ENDPOINT',
'MINIO_SERVER_URL',
defaultValue:
'minio-rsso80cks4ck4oc44s0og80c.147.45.152.129.sslip.io',
),

View file

@ -823,11 +823,8 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
/// Изображение карточки (вынесено в отдельный метод для переиспользования)
Widget _buildCardImage(GameCardDto card, double width, double height) {
// Use presigned URL from DTO if available, fallback to building URL
final imageUrl = card.imageUrl ??
(card.image != null && card.image!.isNotEmpty
? ApiConfigV2.getCardImageUrl(widget.packId, card.id)
: null);
// Use presigned URL from DTO
final imageUrl = card.imageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
return Center(

View file

@ -720,11 +720,8 @@ class _CardSide extends StatelessWidget {
}
Widget _buildImage() {
// Use presigned URL from DTO if available, fallback to building URL
final imageUrl = card.imageUrl ??
(card.image != null && card.image!.isNotEmpty
? ApiConfigV2.getCardImageUrl(packId, card.id)
: null);
// Use presigned URL from DTO
final imageUrl = card.imageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
return Container(

View file

@ -565,11 +565,8 @@ class _CardSide extends StatelessWidget {
}
Widget _buildImage() {
// Use presigned URL from DTO if available, fallback to building URL
final imageUrl = card.imageUrl ??
(card.image != null && card.image!.isNotEmpty
? ApiConfigV2.getCardImageUrl(packId, card.id)
: null);
// Use presigned URL from DTO
final imageUrl = card.imageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
return Container(
@ -603,11 +600,8 @@ class _CardSide extends StatelessWidget {
}
Widget _buildImageBack() {
// Use presigned URL from DTO if available, fallback to building URL
final imageBackUrl = card.imageBackUrl ??
(card.imageBack != null && card.imageBack!.isNotEmpty
? ApiConfigV2.getCardImageBackUrl(packId, card.id)
: null);
// Use presigned URL from DTO
final imageBackUrl = card.imageBackUrl;
if (imageBackUrl == null || imageBackUrl.isEmpty) {
return Container(

View file

@ -260,11 +260,8 @@ class PackCardItem extends StatelessWidget {
/// Отображает изображение карточки
/// Загружает изображение с бэкенда по URL
Widget _buildImage(BuildContext context, double cardWidth, double cardHeight) {
// Use presigned URL from DTO if available, fallback to building URL
final imageUrl = card.imageUrl ??
(card.image != null && card.image!.isNotEmpty
? ApiConfigV2.getCardImageUrl(packId, card.id)
: null);
// Use presigned URL from DTO
final imageUrl = card.imageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
return const SizedBox.shrink();