diff --git a/mnemo_cards_backend/lib/tests/generators/question_generators/matrix_question_generator.dart b/mnemo_cards_backend/lib/tests/generators/question_generators/matrix_question_generator.dart index 34acc46..58f9470 100644 --- a/mnemo_cards_backend/lib/tests/generators/question_generators/matrix_question_generator.dart +++ b/mnemo_cards_backend/lib/tests/generators/question_generators/matrix_question_generator.dart @@ -194,19 +194,31 @@ class MatrixQuestionGenerator implements QuestionGenerator { // Generate stages for all cards final stages = _generateStages(selected, questionType); - // Create cards - always include original and translation for back side display + // Create cards with explicit front and back side fields // Front side will show only relevant fields based on question type, - // but back side needs both original and translation + // Back side always shows original and translation final cards = selected.map((c) { - // Always include original and translation for back side - // Include image if available (for image-based question types) + final frontImage = questionType.imageCards + ? (_imageIdToUrl(c.image) ?? c.image ?? c.id) + : null; + final frontImageUrl = frontImage != null && _isUuid(frontImage) + ? null + : frontImage; + return MatrixCardDto( id: c.id, - image: questionType.imageCards - ? (_imageIdToUrl(c.image) ?? c.image ?? c.id) - : null, + // Front side fields + image: frontImage != null && _isUuid(frontImage) ? frontImage : null, + imageUrl: frontImageUrl, original: c.original, translation: c.translation, + // Back side fields - always include original and translation + originalBack: c.original, + translationBack: c.translation, + imageBack: c.image != null && _isUuid(c.image!) ? c.image : null, + imageBackUrl: c.image != null && !_isUuid(c.image!) + ? c.image + : (c.image != null ? _imageIdToUrl(c.image) : null), ); }).toList(); @@ -225,7 +237,8 @@ class MatrixQuestionGenerator implements QuestionGenerator { stages: stages, // Backward compatibility fields word: firstStage.targetWord, - answer: firstStage.targetCardId, + answer: firstStage + .targetCardId, // Опциональное, но заполняем для совместимости text: 'Найди слово:', ); } diff --git a/mnemo_cards_backend/lib/tests/test_manager.dart b/mnemo_cards_backend/lib/tests/test_manager.dart index efaa43f..9c8864a 100644 --- a/mnemo_cards_backend/lib/tests/test_manager.dart +++ b/mnemo_cards_backend/lib/tests/test_manager.dart @@ -484,10 +484,14 @@ class TestManager { .map( (c) => { 'id': c.id, - // store image as objectId (MinIO) or filename + // Front side fields 'image': c.image, 'original': c.original, 'translation': c.translation, + // Back side fields - always include original and translation + 'originalBack': c.original, + 'translationBack': c.translation, + 'imageBack': c.image, }, ) .toList(); @@ -503,16 +507,56 @@ class TestManager { questionJson['matrixSize'] = resolvedSize; } else if ((questionJson['answer'] as String?)?.isEmpty != false) { // If matrix cards exist but answer is missing, derive initial target - // from the first card. - final first = currentButtons.firstOrNull; - if (first is Map) { - final id = first['id']?.toString(); - final original = first['original']?.toString(); - if (id != null && id.isNotEmpty) { - questionJson['answer'] = id; + // from the first card or from stages. + final stages = (uiDataForResponse['stages'] as List?) ?? []; + if (stages.isNotEmpty) { + // Извлекаем из stages[0] + final firstStage = stages.first; + if (firstStage is Map) { + final targetCardId = firstStage['targetCardId']?.toString(); + final targetWord = firstStage['targetWord']?.toString(); + if (targetCardId != null && targetCardId.isNotEmpty) { + questionJson['answer'] = targetCardId; + } + if (targetWord != null && targetWord.isNotEmpty) { + questionJson['word'] = targetWord; + } } - if (original != null && original.isNotEmpty) { - questionJson['word'] = original; + } else { + // Fallback: извлекаем из первого card + final first = currentButtons.firstOrNull; + if (first is Map) { + final id = first['id']?.toString(); + final original = first['original']?.toString(); + if (id != null && id.isNotEmpty) { + questionJson['answer'] = id; + } + if (original != null && original.isNotEmpty) { + questionJson['word'] = original; + } + } + } + } + + // Также проверяем word и answer из stages, если они пусты из БД + if ((questionJson['word'] as String?)?.isEmpty != false || + (questionJson['answer'] as String?)?.isEmpty != false) { + final stages = (uiDataForResponse['stages'] as List?) ?? []; + if (stages.isNotEmpty) { + final firstStage = stages.first; + if (firstStage is Map) { + if ((questionJson['word'] as String?)?.isEmpty != false) { + final targetWord = firstStage['targetWord']?.toString(); + if (targetWord != null && targetWord.isNotEmpty) { + questionJson['word'] = targetWord; + } + } + if ((questionJson['answer'] as String?)?.isEmpty != false) { + final targetCardId = firstStage['targetCardId']?.toString(); + if (targetCardId != null && targetCardId.isNotEmpty) { + questionJson['answer'] = targetCardId; + } + } } } } @@ -523,19 +567,33 @@ class TestManager { // All images now use the same cardImagesBucket final updatedButtons = await Future.wait( (questionJson['buttons'] as List? ?? []).map((button) async { - if (button is Map && button['image'] != null) { + if (button is Map) { final buttonMap = Map.from(button); - final imageValue = buttonMap['image']?.toString(); - // Convert image to presigned URL - final imageUrl = await _imageValueToApiUrl( - imageValue, - packId: packId, - ); - - if (imageUrl != null) { - buttonMap['imageUrl'] = imageUrl; + // Convert front image to presigned URL + if (buttonMap['image'] != null) { + final imageValue = buttonMap['image']?.toString(); + final imageUrl = await _imageValueToApiUrl( + imageValue, + packId: packId, + ); + if (imageUrl != null) { + buttonMap['imageUrl'] = imageUrl; + } } + + // Convert back image to presigned URL (for matrix cards) + if (buttonMap['imageBack'] != null) { + final imageBackValue = buttonMap['imageBack']?.toString(); + final imageBackUrl = await _imageValueToApiUrl( + imageBackValue, + packId: packId, + ); + if (imageBackUrl != null) { + buttonMap['imageBackUrl'] = imageBackUrl; + } + } + return buttonMap; } return button; @@ -834,8 +892,8 @@ class TestManager { final questionJson = question.toJson(); // Extract key fields - final word = questionJson['word'] as String? ?? ''; - final answer = questionJson['answer'] as String? ?? ''; + var word = questionJson['word'] as String? ?? ''; + var answer = questionJson['answer'] as String? ?? ''; final buttons = questionJson['buttons'] as List? ?? []; // UI data (image, text, audio, template) @@ -852,6 +910,29 @@ class TestManager { if (questionJson['stages'] != null) uiData['stages'] = questionJson['stages']; + // Для матричных вопросов извлекаем word и answer из stages[0] если они пусты + if (question.questionType == TestQuestionType.matrix) { + final stages = (questionJson['stages'] as List?) ?? []; + if (stages.isNotEmpty) { + final firstStage = stages.first; + if (firstStage is Map) { + if (word.isEmpty) { + final targetWord = firstStage['targetWord']?.toString() ?? ''; + if (targetWord.isNotEmpty) { + word = targetWord; + } + } + if (answer.isEmpty) { + final targetCardId = + firstStage['targetCardId']?.toString() ?? ''; + if (targetCardId.isNotEmpty) { + answer = targetCardId; + } + } + } + } + } + final questionCompanion = TestQuestionsCompanion.insert( testId: testId, orderIndex: drift.Value(orderIndex++), diff --git a/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.dart b/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.dart index b29b271..895a4a4 100644 --- a/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.dart +++ b/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.dart @@ -30,7 +30,7 @@ class MatrixTestQuestionBody extends AbstractTestQuestion { /// DEPRECATED: Use stages[0].targetCardId instead. /// Kept for backward compatibility. @JsonKey(defaultValue: '') - final String answer; + final String? answer; /// Question text (e.g., "Найди слово:") final String? text; @@ -40,7 +40,7 @@ class MatrixTestQuestionBody extends AbstractTestQuestion { required this.matrixSize, required this.cards, this.stages = const [], - required this.answer, + this.answer, required super.word, this.text, super.questionType = TestQuestionType.matrix, @@ -80,6 +80,10 @@ class MatrixCardDto { final String? imageUrl; // Presigned URL (for display) final String? original; final String? translation; + final String? originalBack; // Original on back side + final String? translationBack; // Translation on back side + final String? imageBack; // Object ID in MinIO for back side (for admin) + final String? imageBackUrl; // Presigned URL for back side (for display) const MatrixCardDto({ required this.id, @@ -87,6 +91,10 @@ class MatrixCardDto { this.imageUrl, this.original, this.translation, + this.originalBack, + this.translationBack, + this.imageBack, + this.imageBackUrl, }); factory MatrixCardDto.fromJson(Map json) => diff --git a/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.g.dart b/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.g.dart index 56f104b..e25ba6d 100644 --- a/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.g.dart +++ b/mnemo_cards_common/lib/src/dtos/game_tests/test_bodies/matrix_test_question_body.g.dart @@ -15,7 +15,7 @@ abstract class _$MatrixTestQuestionBodyCWProxy { MatrixTestQuestionBody stages(List stages); - MatrixTestQuestionBody answer(String answer); + MatrixTestQuestionBody answer(String? answer); MatrixTestQuestionBody word(String word); @@ -35,7 +35,7 @@ abstract class _$MatrixTestQuestionBodyCWProxy { int matrixSize, List cards, List stages, - String answer, + String? answer, String word, String? text, TestQuestionType questionType, @@ -65,7 +65,7 @@ class _$MatrixTestQuestionBodyCWProxyImpl call(stages: stages); @override - MatrixTestQuestionBody answer(String answer) => call(answer: answer); + MatrixTestQuestionBody answer(String? answer) => call(answer: answer); @override MatrixTestQuestionBody word(String word) => call(word: word); @@ -113,10 +113,10 @@ class _$MatrixTestQuestionBodyCWProxyImpl ? _value.stages // ignore: cast_nullable_to_non_nullable : stages as List, - answer: answer == const $CopyWithPlaceholder() || answer == null + answer: answer == const $CopyWithPlaceholder() ? _value.answer // ignore: cast_nullable_to_non_nullable - : answer as String, + : answer as String?, word: word == const $CopyWithPlaceholder() || word == null ? _value.word // ignore: cast_nullable_to_non_nullable @@ -231,6 +231,14 @@ abstract class _$MatrixCardDtoCWProxy { MatrixCardDto translation(String? translation); + MatrixCardDto originalBack(String? originalBack); + + MatrixCardDto translationBack(String? translationBack); + + MatrixCardDto imageBack(String? imageBack); + + MatrixCardDto imageBackUrl(String? imageBackUrl); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `MatrixCardDto(...).copyWith.fieldName(value)`. /// @@ -244,6 +252,10 @@ abstract class _$MatrixCardDtoCWProxy { String? imageUrl, String? original, String? translation, + String? originalBack, + String? translationBack, + String? imageBack, + String? imageBackUrl, }); } @@ -270,6 +282,21 @@ class _$MatrixCardDtoCWProxyImpl implements _$MatrixCardDtoCWProxy { MatrixCardDto translation(String? translation) => call(translation: translation); + @override + MatrixCardDto originalBack(String? originalBack) => + call(originalBack: originalBack); + + @override + MatrixCardDto translationBack(String? translationBack) => + call(translationBack: translationBack); + + @override + MatrixCardDto imageBack(String? imageBack) => call(imageBack: imageBack); + + @override + MatrixCardDto imageBackUrl(String? imageBackUrl) => + call(imageBackUrl: imageBackUrl); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `MatrixCardDto(...).copyWith.fieldName(value)`. @@ -284,6 +311,10 @@ class _$MatrixCardDtoCWProxyImpl implements _$MatrixCardDtoCWProxy { Object? imageUrl = const $CopyWithPlaceholder(), Object? original = const $CopyWithPlaceholder(), Object? translation = const $CopyWithPlaceholder(), + Object? originalBack = const $CopyWithPlaceholder(), + Object? translationBack = const $CopyWithPlaceholder(), + Object? imageBack = const $CopyWithPlaceholder(), + Object? imageBackUrl = const $CopyWithPlaceholder(), }) { return MatrixCardDto( id: id == const $CopyWithPlaceholder() || id == null @@ -306,6 +337,22 @@ class _$MatrixCardDtoCWProxyImpl implements _$MatrixCardDtoCWProxy { ? _value.translation // ignore: cast_nullable_to_non_nullable : translation as String?, + originalBack: originalBack == const $CopyWithPlaceholder() + ? _value.originalBack + // ignore: cast_nullable_to_non_nullable + : originalBack as String?, + translationBack: translationBack == const $CopyWithPlaceholder() + ? _value.translationBack + // ignore: cast_nullable_to_non_nullable + : translationBack as String?, + imageBack: imageBack == const $CopyWithPlaceholder() + ? _value.imageBack + // ignore: cast_nullable_to_non_nullable + : imageBack as String?, + imageBackUrl: imageBackUrl == const $CopyWithPlaceholder() + ? _value.imageBackUrl + // ignore: cast_nullable_to_non_nullable + : imageBackUrl as String?, ); } } @@ -353,7 +400,7 @@ Map _$MatrixTestQuestionBodyToJson( 'matrixSize': instance.matrixSize, 'buttons': instance.cards.map((e) => e.toJson()).toList(), 'stages': instance.stages.map((e) => e.toJson()).toList(), - 'answer': instance.answer, + 'answer': ?instance.answer, 'text': ?instance.text, }; @@ -386,6 +433,10 @@ MatrixCardDto _$MatrixCardDtoFromJson(Map json) => imageUrl: json['imageUrl'] as String?, original: json['original'] as String?, translation: json['translation'] as String?, + originalBack: json['originalBack'] as String?, + translationBack: json['translationBack'] as String?, + imageBack: json['imageBack'] as String?, + imageBackUrl: json['imageBackUrl'] as String?, ); Map _$MatrixCardDtoToJson(MatrixCardDto instance) => @@ -395,4 +446,8 @@ Map _$MatrixCardDtoToJson(MatrixCardDto instance) => 'imageUrl': instance.imageUrl, 'original': instance.original, 'translation': instance.translation, + 'originalBack': instance.originalBack, + 'translationBack': instance.translationBack, + 'imageBack': instance.imageBack, + 'imageBackUrl': instance.imageBackUrl, }; diff --git a/mnemo_cards_web_v2/lib/domain/models/game_question.dart b/mnemo_cards_web_v2/lib/domain/models/game_question.dart index 468b8cc..ffb3236 100644 --- a/mnemo_cards_web_v2/lib/domain/models/game_question.dart +++ b/mnemo_cards_web_v2/lib/domain/models/game_question.dart @@ -121,6 +121,9 @@ abstract class MatchPair with _$MatchPair { /// The question is multi-step on a single screen: after each correct selection /// the chosen card flips and a new target is shown from stages array until /// all cards are processed. +/// +/// Note: Each stage has its own targetWord. For backward compatibility, +/// use stages[0].targetWord if a single word is needed. @freezed abstract class MatrixQuestion with _$MatrixQuestion { const factory MatrixQuestion({ @@ -131,7 +134,6 @@ abstract class MatrixQuestion with _$MatrixQuestion { // DEPRECATED: Use stages[0] instead. Kept for backward compatibility. @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, - required String word, String? text, @Default('matrix') String type, }) = _MatrixQuestion; @@ -157,8 +159,13 @@ abstract class MatrixCard with _$MatrixCard { const factory MatrixCard({ required String id, String? image, + String? imageUrl, String? original, String? translation, + String? originalBack, + String? translationBack, + String? imageBack, + String? imageBackUrl, }) = _MatrixCard; factory MatrixCard.fromJson(Map json) => diff --git a/mnemo_cards_web_v2/lib/domain/models/game_question.freezed.dart b/mnemo_cards_web_v2/lib/domain/models/game_question.freezed.dart index c9fa9d2..96fca1f 100644 --- a/mnemo_cards_web_v2/lib/domain/models/game_question.freezed.dart +++ b/mnemo_cards_web_v2/lib/domain/models/game_question.freezed.dart @@ -2268,7 +2268,7 @@ as String, mixin _$MatrixQuestion { String get id; int get matrixSize; List get cards; List get stages;// DEPRECATED: Use stages[0] instead. Kept for backward compatibility. -@Deprecated('Use stages[0] instead') String? get initialTargetCardId;@Deprecated('Use stages[0] instead') String? get initialTargetWord; String get word; String? get text; String get type; +@Deprecated('Use stages[0] instead') String? get initialTargetCardId;@Deprecated('Use stages[0] instead') String? get initialTargetWord; String? get text; String get type; /// Create a copy of MatrixQuestion /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -2281,16 +2281,16 @@ $MatrixQuestionCopyWith get copyWith => _$MatrixQuestionCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is MatrixQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.matrixSize, matrixSize) || other.matrixSize == matrixSize)&&const DeepCollectionEquality().equals(other.cards, cards)&&const DeepCollectionEquality().equals(other.stages, stages)&&(identical(other.initialTargetCardId, initialTargetCardId) || other.initialTargetCardId == initialTargetCardId)&&(identical(other.initialTargetWord, initialTargetWord) || other.initialTargetWord == initialTargetWord)&&(identical(other.word, word) || other.word == word)&&(identical(other.text, text) || other.text == text)&&(identical(other.type, type) || other.type == type)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is MatrixQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.matrixSize, matrixSize) || other.matrixSize == matrixSize)&&const DeepCollectionEquality().equals(other.cards, cards)&&const DeepCollectionEquality().equals(other.stages, stages)&&(identical(other.initialTargetCardId, initialTargetCardId) || other.initialTargetCardId == initialTargetCardId)&&(identical(other.initialTargetWord, initialTargetWord) || other.initialTargetWord == initialTargetWord)&&(identical(other.text, text) || other.text == text)&&(identical(other.type, type) || other.type == type)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,matrixSize,const DeepCollectionEquality().hash(cards),const DeepCollectionEquality().hash(stages),initialTargetCardId,initialTargetWord,word,text,type); +int get hashCode => Object.hash(runtimeType,id,matrixSize,const DeepCollectionEquality().hash(cards),const DeepCollectionEquality().hash(stages),initialTargetCardId,initialTargetWord,text,type); @override String toString() { - return 'MatrixQuestion(id: $id, matrixSize: $matrixSize, cards: $cards, stages: $stages, initialTargetCardId: $initialTargetCardId, initialTargetWord: $initialTargetWord, word: $word, text: $text, type: $type)'; + return 'MatrixQuestion(id: $id, matrixSize: $matrixSize, cards: $cards, stages: $stages, initialTargetCardId: $initialTargetCardId, initialTargetWord: $initialTargetWord, text: $text, type: $type)'; } @@ -2301,7 +2301,7 @@ abstract mixin class $MatrixQuestionCopyWith<$Res> { factory $MatrixQuestionCopyWith(MatrixQuestion value, $Res Function(MatrixQuestion) _then) = _$MatrixQuestionCopyWithImpl; @useResult $Res call({ - String id, int matrixSize, List cards, List stages,@Deprecated('Use stages[0] instead') String? initialTargetCardId,@Deprecated('Use stages[0] instead') String? initialTargetWord, String word, String? text, String type + String id, int matrixSize, List cards, List stages,@Deprecated('Use stages[0] instead') String? initialTargetCardId,@Deprecated('Use stages[0] instead') String? initialTargetWord, String? text, String type }); @@ -2318,7 +2318,7 @@ class _$MatrixQuestionCopyWithImpl<$Res> /// Create a copy of MatrixQuestion /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? matrixSize = null,Object? cards = null,Object? stages = null,Object? initialTargetCardId = freezed,Object? initialTargetWord = freezed,Object? word = null,Object? text = freezed,Object? type = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? matrixSize = null,Object? cards = null,Object? stages = null,Object? initialTargetCardId = freezed,Object? initialTargetWord = freezed,Object? text = freezed,Object? type = null,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,matrixSize: null == matrixSize ? _self.matrixSize : matrixSize // ignore: cast_nullable_to_non_nullable @@ -2326,8 +2326,7 @@ as int,cards: null == cards ? _self.cards : cards // ignore: cast_nullable_to_no as List,stages: null == stages ? _self.stages : stages // ignore: cast_nullable_to_non_nullable as List,initialTargetCardId: freezed == initialTargetCardId ? _self.initialTargetCardId : initialTargetCardId // ignore: cast_nullable_to_non_nullable as String?,initialTargetWord: freezed == initialTargetWord ? _self.initialTargetWord : initialTargetWord // ignore: cast_nullable_to_non_nullable -as String?,word: null == word ? _self.word : word // ignore: cast_nullable_to_non_nullable -as String,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String, )); @@ -2414,10 +2413,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String word, String? text, String type)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String? text, String type)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _MatrixQuestion() when $default != null: -return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.word,_that.text,_that.type);case _: +return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.text,_that.type);case _: return orElse(); } @@ -2435,10 +2434,10 @@ return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initial /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String word, String? text, String type) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String? text, String type) $default,) {final _that = this; switch (_that) { case _MatrixQuestion(): -return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.word,_that.text,_that.type);case _: +return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.text,_that.type);case _: throw StateError('Unexpected subclass'); } @@ -2455,10 +2454,10 @@ return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initial /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String word, String? text, String type)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int matrixSize, List cards, List stages, @Deprecated('Use stages[0] instead') String? initialTargetCardId, @Deprecated('Use stages[0] instead') String? initialTargetWord, String? text, String type)? $default,) {final _that = this; switch (_that) { case _MatrixQuestion() when $default != null: -return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.word,_that.text,_that.type);case _: +return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initialTargetCardId,_that.initialTargetWord,_that.text,_that.type);case _: return null; } @@ -2470,7 +2469,7 @@ return $default(_that.id,_that.matrixSize,_that.cards,_that.stages,_that.initial @JsonSerializable() class _MatrixQuestion implements MatrixQuestion { - const _MatrixQuestion({required this.id, required this.matrixSize, required final List cards, final List stages = const [], @Deprecated('Use stages[0] instead') this.initialTargetCardId, @Deprecated('Use stages[0] instead') this.initialTargetWord, required this.word, this.text, this.type = 'matrix'}): _cards = cards,_stages = stages; + const _MatrixQuestion({required this.id, required this.matrixSize, required final List cards, final List stages = const [], @Deprecated('Use stages[0] instead') this.initialTargetCardId, @Deprecated('Use stages[0] instead') this.initialTargetWord, this.text, this.type = 'matrix'}): _cards = cards,_stages = stages; factory _MatrixQuestion.fromJson(Map json) => _$MatrixQuestionFromJson(json); @override final String id; @@ -2492,7 +2491,6 @@ class _MatrixQuestion implements MatrixQuestion { // DEPRECATED: Use stages[0] instead. Kept for backward compatibility. @override@Deprecated('Use stages[0] instead') final String? initialTargetCardId; @override@Deprecated('Use stages[0] instead') final String? initialTargetWord; -@override final String word; @override final String? text; @override@JsonKey() final String type; @@ -2509,16 +2507,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.matrixSize, matrixSize) || other.matrixSize == matrixSize)&&const DeepCollectionEquality().equals(other._cards, _cards)&&const DeepCollectionEquality().equals(other._stages, _stages)&&(identical(other.initialTargetCardId, initialTargetCardId) || other.initialTargetCardId == initialTargetCardId)&&(identical(other.initialTargetWord, initialTargetWord) || other.initialTargetWord == initialTargetWord)&&(identical(other.word, word) || other.word == word)&&(identical(other.text, text) || other.text == text)&&(identical(other.type, type) || other.type == type)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.matrixSize, matrixSize) || other.matrixSize == matrixSize)&&const DeepCollectionEquality().equals(other._cards, _cards)&&const DeepCollectionEquality().equals(other._stages, _stages)&&(identical(other.initialTargetCardId, initialTargetCardId) || other.initialTargetCardId == initialTargetCardId)&&(identical(other.initialTargetWord, initialTargetWord) || other.initialTargetWord == initialTargetWord)&&(identical(other.text, text) || other.text == text)&&(identical(other.type, type) || other.type == type)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,matrixSize,const DeepCollectionEquality().hash(_cards),const DeepCollectionEquality().hash(_stages),initialTargetCardId,initialTargetWord,word,text,type); +int get hashCode => Object.hash(runtimeType,id,matrixSize,const DeepCollectionEquality().hash(_cards),const DeepCollectionEquality().hash(_stages),initialTargetCardId,initialTargetWord,text,type); @override String toString() { - return 'MatrixQuestion(id: $id, matrixSize: $matrixSize, cards: $cards, stages: $stages, initialTargetCardId: $initialTargetCardId, initialTargetWord: $initialTargetWord, word: $word, text: $text, type: $type)'; + return 'MatrixQuestion(id: $id, matrixSize: $matrixSize, cards: $cards, stages: $stages, initialTargetCardId: $initialTargetCardId, initialTargetWord: $initialTargetWord, text: $text, type: $type)'; } @@ -2529,7 +2527,7 @@ abstract mixin class _$MatrixQuestionCopyWith<$Res> implements $MatrixQuestionCo factory _$MatrixQuestionCopyWith(_MatrixQuestion value, $Res Function(_MatrixQuestion) _then) = __$MatrixQuestionCopyWithImpl; @override @useResult $Res call({ - String id, int matrixSize, List cards, List stages,@Deprecated('Use stages[0] instead') String? initialTargetCardId,@Deprecated('Use stages[0] instead') String? initialTargetWord, String word, String? text, String type + String id, int matrixSize, List cards, List stages,@Deprecated('Use stages[0] instead') String? initialTargetCardId,@Deprecated('Use stages[0] instead') String? initialTargetWord, String? text, String type }); @@ -2546,7 +2544,7 @@ class __$MatrixQuestionCopyWithImpl<$Res> /// Create a copy of MatrixQuestion /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? matrixSize = null,Object? cards = null,Object? stages = null,Object? initialTargetCardId = freezed,Object? initialTargetWord = freezed,Object? word = null,Object? text = freezed,Object? type = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? matrixSize = null,Object? cards = null,Object? stages = null,Object? initialTargetCardId = freezed,Object? initialTargetWord = freezed,Object? text = freezed,Object? type = null,}) { return _then(_MatrixQuestion( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,matrixSize: null == matrixSize ? _self.matrixSize : matrixSize // ignore: cast_nullable_to_non_nullable @@ -2554,8 +2552,7 @@ as int,cards: null == cards ? _self._cards : cards // ignore: cast_nullable_to_n as List,stages: null == stages ? _self._stages : stages // ignore: cast_nullable_to_non_nullable as List,initialTargetCardId: freezed == initialTargetCardId ? _self.initialTargetCardId : initialTargetCardId // ignore: cast_nullable_to_non_nullable as String?,initialTargetWord: freezed == initialTargetWord ? _self.initialTargetWord : initialTargetWord // ignore: cast_nullable_to_non_nullable -as String?,word: null == word ? _self.word : word // ignore: cast_nullable_to_non_nullable -as String,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String, )); @@ -2837,7 +2834,7 @@ as String?, /// @nodoc mixin _$MatrixCard { - String get id; String? get image; String? get original; String? get translation; + String get id; String? get image; String? get imageUrl; String? get original; String? get translation; String? get originalBack; String? get translationBack; String? get imageBack; String? get imageBackUrl; /// Create a copy of MatrixCard /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -2850,16 +2847,16 @@ $MatrixCardCopyWith get copyWith => _$MatrixCardCopyWithImpl Object.hash(runtimeType,id,image,original,translation); +int get hashCode => Object.hash(runtimeType,id,image,imageUrl,original,translation,originalBack,translationBack,imageBack,imageBackUrl); @override String toString() { - return 'MatrixCard(id: $id, image: $image, original: $original, translation: $translation)'; + return 'MatrixCard(id: $id, image: $image, imageUrl: $imageUrl, original: $original, translation: $translation, originalBack: $originalBack, translationBack: $translationBack, imageBack: $imageBack, imageBackUrl: $imageBackUrl)'; } @@ -2870,7 +2867,7 @@ abstract mixin class $MatrixCardCopyWith<$Res> { factory $MatrixCardCopyWith(MatrixCard value, $Res Function(MatrixCard) _then) = _$MatrixCardCopyWithImpl; @useResult $Res call({ - String id, String? image, String? original, String? translation + String id, String? image, String? imageUrl, String? original, String? translation, String? originalBack, String? translationBack, String? imageBack, String? imageBackUrl }); @@ -2887,12 +2884,17 @@ class _$MatrixCardCopyWithImpl<$Res> /// Create a copy of MatrixCard /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? image = freezed,Object? original = freezed,Object? translation = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? image = freezed,Object? imageUrl = freezed,Object? original = freezed,Object? translation = freezed,Object? originalBack = freezed,Object? translationBack = freezed,Object? imageBack = freezed,Object? imageBackUrl = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,image: freezed == image ? _self.image : image // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?,original: freezed == original ? _self.original : original // ignore: cast_nullable_to_non_nullable as String?,translation: freezed == translation ? _self.translation : translation // ignore: cast_nullable_to_non_nullable +as String?,originalBack: freezed == originalBack ? _self.originalBack : originalBack // ignore: cast_nullable_to_non_nullable +as String?,translationBack: freezed == translationBack ? _self.translationBack : translationBack // ignore: cast_nullable_to_non_nullable +as String?,imageBack: freezed == imageBack ? _self.imageBack : imageBack // ignore: cast_nullable_to_non_nullable +as String?,imageBackUrl: freezed == imageBackUrl ? _self.imageBackUrl : imageBackUrl // ignore: cast_nullable_to_non_nullable as String?, )); } @@ -2978,10 +2980,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? image, String? original, String? translation)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? image, String? imageUrl, String? original, String? translation, String? originalBack, String? translationBack, String? imageBack, String? imageBackUrl)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _MatrixCard() when $default != null: -return $default(_that.id,_that.image,_that.original,_that.translation);case _: +return $default(_that.id,_that.image,_that.imageUrl,_that.original,_that.translation,_that.originalBack,_that.translationBack,_that.imageBack,_that.imageBackUrl);case _: return orElse(); } @@ -2999,10 +3001,10 @@ return $default(_that.id,_that.image,_that.original,_that.translation);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String? image, String? original, String? translation) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String? image, String? imageUrl, String? original, String? translation, String? originalBack, String? translationBack, String? imageBack, String? imageBackUrl) $default,) {final _that = this; switch (_that) { case _MatrixCard(): -return $default(_that.id,_that.image,_that.original,_that.translation);case _: +return $default(_that.id,_that.image,_that.imageUrl,_that.original,_that.translation,_that.originalBack,_that.translationBack,_that.imageBack,_that.imageBackUrl);case _: throw StateError('Unexpected subclass'); } @@ -3019,10 +3021,10 @@ return $default(_that.id,_that.image,_that.original,_that.translation);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? image, String? original, String? translation)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? image, String? imageUrl, String? original, String? translation, String? originalBack, String? translationBack, String? imageBack, String? imageBackUrl)? $default,) {final _that = this; switch (_that) { case _MatrixCard() when $default != null: -return $default(_that.id,_that.image,_that.original,_that.translation);case _: +return $default(_that.id,_that.image,_that.imageUrl,_that.original,_that.translation,_that.originalBack,_that.translationBack,_that.imageBack,_that.imageBackUrl);case _: return null; } @@ -3034,13 +3036,18 @@ return $default(_that.id,_that.image,_that.original,_that.translation);case _: @JsonSerializable() class _MatrixCard implements MatrixCard { - const _MatrixCard({required this.id, this.image, this.original, this.translation}); + const _MatrixCard({required this.id, this.image, this.imageUrl, this.original, this.translation, this.originalBack, this.translationBack, this.imageBack, this.imageBackUrl}); factory _MatrixCard.fromJson(Map json) => _$MatrixCardFromJson(json); @override final String id; @override final String? image; +@override final String? imageUrl; @override final String? original; @override final String? translation; +@override final String? originalBack; +@override final String? translationBack; +@override final String? imageBack; +@override final String? imageBackUrl; /// Create a copy of MatrixCard /// with the given fields replaced by the non-null parameter values. @@ -3055,16 +3062,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixCard&&(identical(other.id, id) || other.id == id)&&(identical(other.image, image) || other.image == image)&&(identical(other.original, original) || other.original == original)&&(identical(other.translation, translation) || other.translation == translation)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MatrixCard&&(identical(other.id, id) || other.id == id)&&(identical(other.image, image) || other.image == image)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.original, original) || other.original == original)&&(identical(other.translation, translation) || other.translation == translation)&&(identical(other.originalBack, originalBack) || other.originalBack == originalBack)&&(identical(other.translationBack, translationBack) || other.translationBack == translationBack)&&(identical(other.imageBack, imageBack) || other.imageBack == imageBack)&&(identical(other.imageBackUrl, imageBackUrl) || other.imageBackUrl == imageBackUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,image,original,translation); +int get hashCode => Object.hash(runtimeType,id,image,imageUrl,original,translation,originalBack,translationBack,imageBack,imageBackUrl); @override String toString() { - return 'MatrixCard(id: $id, image: $image, original: $original, translation: $translation)'; + return 'MatrixCard(id: $id, image: $image, imageUrl: $imageUrl, original: $original, translation: $translation, originalBack: $originalBack, translationBack: $translationBack, imageBack: $imageBack, imageBackUrl: $imageBackUrl)'; } @@ -3075,7 +3082,7 @@ abstract mixin class _$MatrixCardCopyWith<$Res> implements $MatrixCardCopyWith<$ factory _$MatrixCardCopyWith(_MatrixCard value, $Res Function(_MatrixCard) _then) = __$MatrixCardCopyWithImpl; @override @useResult $Res call({ - String id, String? image, String? original, String? translation + String id, String? image, String? imageUrl, String? original, String? translation, String? originalBack, String? translationBack, String? imageBack, String? imageBackUrl }); @@ -3092,12 +3099,17 @@ class __$MatrixCardCopyWithImpl<$Res> /// Create a copy of MatrixCard /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? image = freezed,Object? original = freezed,Object? translation = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? image = freezed,Object? imageUrl = freezed,Object? original = freezed,Object? translation = freezed,Object? originalBack = freezed,Object? translationBack = freezed,Object? imageBack = freezed,Object? imageBackUrl = freezed,}) { return _then(_MatrixCard( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,image: freezed == image ? _self.image : image // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable as String?,original: freezed == original ? _self.original : original // ignore: cast_nullable_to_non_nullable as String?,translation: freezed == translation ? _self.translation : translation // ignore: cast_nullable_to_non_nullable +as String?,originalBack: freezed == originalBack ? _self.originalBack : originalBack // ignore: cast_nullable_to_non_nullable +as String?,translationBack: freezed == translationBack ? _self.translationBack : translationBack // ignore: cast_nullable_to_non_nullable +as String?,imageBack: freezed == imageBack ? _self.imageBack : imageBack // ignore: cast_nullable_to_non_nullable +as String?,imageBackUrl: freezed == imageBackUrl ? _self.imageBackUrl : imageBackUrl // ignore: cast_nullable_to_non_nullable as String?, )); } diff --git a/mnemo_cards_web_v2/lib/domain/models/game_question.g.dart b/mnemo_cards_web_v2/lib/domain/models/game_question.g.dart index 6e72111..a8ef733 100644 --- a/mnemo_cards_web_v2/lib/domain/models/game_question.g.dart +++ b/mnemo_cards_web_v2/lib/domain/models/game_question.g.dart @@ -205,7 +205,6 @@ _MatrixQuestion _$MatrixQuestionFromJson(Map json) => const [], initialTargetCardId: json['initialTargetCardId'] as String?, initialTargetWord: json['initialTargetWord'] as String?, - word: json['word'] as String, text: json['text'] as String?, type: json['type'] as String? ?? 'matrix', ); @@ -218,7 +217,6 @@ Map _$MatrixQuestionToJson(_MatrixQuestion instance) => 'stages': instance.stages, 'initialTargetCardId': instance.initialTargetCardId, 'initialTargetWord': instance.initialTargetWord, - 'word': instance.word, 'text': instance.text, 'type': instance.type, }; @@ -239,16 +237,26 @@ Map _$MatrixStageToJson(_MatrixStage instance) => _MatrixCard _$MatrixCardFromJson(Map json) => _MatrixCard( id: json['id'] as String, image: json['image'] as String?, + imageUrl: json['imageUrl'] as String?, original: json['original'] as String?, translation: json['translation'] as String?, + originalBack: json['originalBack'] as String?, + translationBack: json['translationBack'] as String?, + imageBack: json['imageBack'] as String?, + imageBackUrl: json['imageBackUrl'] as String?, ); Map _$MatrixCardToJson(_MatrixCard instance) => { 'id': instance.id, 'image': instance.image, + 'imageUrl': instance.imageUrl, 'original': instance.original, 'translation': instance.translation, + 'originalBack': instance.originalBack, + 'translationBack': instance.translationBack, + 'imageBack': instance.imageBack, + 'imageBackUrl': instance.imageBackUrl, }; _QuestionResult _$QuestionResultFromJson(Map json) => diff --git a/mnemo_cards_web_v2/lib/domain/services/game_session_manager.dart b/mnemo_cards_web_v2/lib/domain/services/game_session_manager.dart index f3495e5..8e64c2c 100644 --- a/mnemo_cards_web_v2/lib/domain/services/game_session_manager.dart +++ b/mnemo_cards_web_v2/lib/domain/services/game_session_manager.dart @@ -11,7 +11,7 @@ class GameSessionManager { GameSessionManager(); GameSessionResult? _currentResult; - final Map _questionResults = {}; + final Map> _questionResults = {}; Timer? _questionTimer; DateTime? _sessionStartTime; DateTime? _currentQuestionStartTime; @@ -21,7 +21,7 @@ class GameSessionManager { GameSessionResult? get currentResult => _currentResult; /// All question results for current session - Map get questionResults => + Map> get questionResults => Map.unmodifiable(_questionResults); /// Elapsed time since session start @@ -50,11 +50,53 @@ class GameSessionManager { // Pre-populate question results with placeholders for (final question in questions) { final questionId = _getQuestionId(question); - _questionResults[questionId] = QuestionResult( - questionId: questionId, - word: _getQuestionWord(question), - isCorrect: false, - timeSpent: Duration.zero, + + question.when( + multipleChoice: (_) { + // Для обычных: список с одним элементом + _questionResults[questionId] = [ + QuestionResult( + questionId: questionId, + word: _getQuestionWord(question), + isCorrect: false, + timeSpent: Duration.zero, + ) + ]; + }, + inputLetters: (_) { + // Для обычных: список с одним элементом + _questionResults[questionId] = [ + QuestionResult( + questionId: questionId, + word: _getQuestionWord(question), + isCorrect: false, + timeSpent: Duration.zero, + ) + ]; + }, + match: (_) { + // Для обычных: список с одним элементом + _questionResults[questionId] = [ + QuestionResult( + questionId: questionId, + word: _getQuestionWord(question), + isCorrect: false, + timeSpent: Duration.zero, + ) + ]; + }, + matrix: (q) { + // Для матричных: список placeholders для каждого stage + _questionResults[questionId] = q.stages.map((stage) => + QuestionResult( + questionId: questionId, + word: stage.targetWord, + isCorrect: false, + timeSpent: Duration.zero, + selectedAnswer: stage.targetCardId, // Для идентификации + ) + ).toList(); + }, ); } @@ -74,31 +116,69 @@ class GameSessionManager { /// Submit answer for current question and move to next void submitAnswer(String questionId, GameQuestion question, dynamic answer) { final timeSpent = _calculateTimeSpent(); - final isCorrect = _validateAnswer(question, answer); log( - 'Answer submitted for question $questionId: correct=$isCorrect, time=${timeSpent.inMilliseconds}ms', + 'Answer submitted for question $questionId, time=${timeSpent.inMilliseconds}ms', name: 'GameSessionManager', ); - final selectedAnswer = answer is String ? answer : null; - final selectedAnswers = switch (answer) { - List v => v, - MatrixImageSelectAnswer v => v.correctCardIdsInOrder, - _ => null, - }; - - final result = QuestionResult( - questionId: questionId, - word: _getQuestionWord(question), - isCorrect: isCorrect, - timeSpent: timeSpent, - selectedAnswer: selectedAnswer, - selectedAnswers: selectedAnswers, - answeredAt: DateTime.now(), + // Для матричных вопросов создаем список результатов (по одному на stage) + var isMatrixProcessed = false; + question.when( + matrix: (q) { + if (answer is MatrixImageSelectAnswer) { + final results = []; + final timePerStageMs = timeSpent.inMilliseconds / q.stages.length; + final timePerStage = Duration(milliseconds: timePerStageMs.round()); + + for (int i = 0; i < q.stages.length; i++) { + final stage = q.stages[i]; + final selectedCardId = answer.correctCardIdsInOrder[i]; + final isStageCorrect = selectedCardId == stage.targetCardId; + + results.add(QuestionResult( + questionId: questionId, // Оригинальный ID (без _stage_) + word: stage.targetWord, + isCorrect: isStageCorrect, + timeSpent: timePerStage, + selectedAnswer: selectedCardId, + selectedAnswers: answer.correctCardIdsInOrder, + answeredAt: DateTime.now(), + )); + } + + _questionResults[questionId] = results; + isMatrixProcessed = true; + } + }, + multipleChoice: (_) {}, + inputLetters: (_) {}, + match: (_) {}, ); + + if (!isMatrixProcessed) { + // Обычные вопросы: список с одним элементом + final isCorrect = _validateAnswer(question, answer); + final selectedAnswer = answer is String ? answer : null; + final selectedAnswers = switch (answer) { + List v => v, + MatrixImageSelectAnswer v => v.correctCardIdsInOrder, + _ => null, + }; + + final result = QuestionResult( + questionId: questionId, + word: _getQuestionWord(question), + isCorrect: isCorrect, + timeSpent: timeSpent, + selectedAnswer: selectedAnswer, + selectedAnswers: selectedAnswers, + answeredAt: DateTime.now(), + ); + + _questionResults[questionId] = [result]; + } - _questionResults[questionId] = result; _questionTimer?.cancel(); _currentQuestionStartTime = null; } @@ -108,14 +188,20 @@ class GameSessionManager { /// Complete the current game session GameSessionResult completeSession(String testId) { final totalTime = _calculateSessionTime(); - final correctAnswers = _questionResults.values + + // Все результаты из всех списков + final allResults = _questionResults.values + .expand((results) => results) + .toList(); + + final correctAnswers = allResults .where((r) => r.isCorrect) .length; - final totalQuestions = _questionResults.length; + final totalQuestions = allResults.length; // Количество stages _currentResult = GameSessionResult( testId: testId, - questionResults: _questionResults.values.toList(), + questionResults: allResults, // Плоский список totalTime: totalTime, correctAnswers: correctAnswers, totalQuestions: totalQuestions, @@ -136,21 +222,51 @@ class GameSessionManager { log('Question skipped: $questionId', name: 'GameSessionManager'); final timeSpent = _calculateTimeSpent(); - final existingResult = _questionResults[questionId]; + final results = _questionResults[questionId]; - if (existingResult != null) { - _questionResults[questionId] = existingResult.copyWith( - timeSpent: existingResult.timeSpent + timeSpent, - ); + if (results != null && results.isNotEmpty) { + final timePerResultMs = timeSpent.inMilliseconds / results.length; + final timePerResult = Duration(milliseconds: timePerResultMs.round()); + _questionResults[questionId] = results + .map((r) => r.copyWith(timeSpent: r.timeSpent + timePerResult)) + .toList(); } _questionTimer?.cancel(); _currentQuestionStartTime = null; } - /// Get result for specific question + /// Get result for specific question (returns first result for backward compatibility) QuestionResult? getQuestionResult(String questionId) { - return _questionResults[questionId]; + final results = _questionResults[questionId]; + return results?.isNotEmpty == true ? results!.first : null; + } + + /// Get result for specific stage of matrix question + QuestionResult? getQuestionResultByStage( + String questionId, + String word, + String? selectedAnswer, + ) { + final results = _questionResults[questionId]; + if (results == null || results.isEmpty) return null; + + try { + return results.firstWhere( + (r) => r.word == word && r.selectedAnswer == selectedAnswer, + ); + } catch (_) { + return null; + } + } + + /// Check if all stages of matrix question are correct + bool isMatrixQuestionCorrect(String questionId) { + final results = _questionResults[questionId]; + if (results == null || results.isEmpty) return false; + + // Все stages должны быть правильными + return results.every((r) => r.isCorrect); } /// Check if session is active @@ -160,10 +276,14 @@ class GameSessionManager { Map getSessionStats() { if (!isSessionActive) return {}; - final answeredQuestions = _questionResults.values + final allResults = _questionResults.values + .expand((results) => results) + .toList(); + + final answeredQuestions = allResults .where((r) => r.answeredAt != null) .length; - final correctAnswers = _questionResults.values + final correctAnswers = allResults .where((r) => r.isCorrect) .length; final totalTime = _calculateSessionTime(); @@ -171,7 +291,7 @@ class GameSessionManager { return { 'answeredQuestions': answeredQuestions, 'correctAnswers': correctAnswers, - 'totalQuestions': _questionResults.length, + 'totalQuestions': allResults.length, // Количество stages 'accuracy': answeredQuestions > 0 ? correctAnswers / answeredQuestions : 0.0, @@ -206,7 +326,7 @@ class GameSessionManager { multipleChoice: (q) => q.word, inputLetters: (q) => q.word, match: (q) => q.word, - matrix: (q) => q.word, + matrix: (q) => q.stages.isNotEmpty ? q.stages.first.targetWord : '', ); } @@ -283,13 +403,25 @@ class GameSessionManager { // attempts. Progression to the next question is handled by // TestsStateManager (it should advance even if isCorrect == false). if (answer is MatrixImageSelectAnswer) { - return answer.correctCardIdsInOrder.length == question.cards.length && - answer.wrongAttempts == 0; + // Проверяем, что все stages пройдены + if (answer.correctCardIdsInOrder.length != question.stages.length) { + return false; + } + + // Проверяем, что все stages правильные + for (int i = 0; i < question.stages.length; i++) { + if (answer.correctCardIdsInOrder[i] != question.stages[i].targetCardId) { + return false; + } + } + + // Проверяем, что не было ошибок + return answer.wrongAttempts == 0; } // Backward/compat: if someone submits just the correct ids list. if (answer is List) { - return answer.length == question.cards.length; + return answer.length == question.stages.length; } return false; diff --git a/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.dart b/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.dart index a57a8ea..03a1c37 100644 --- a/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.dart +++ b/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.dart @@ -36,7 +36,7 @@ class TestsState with _$TestsState { required List questions, required int currentQuestionIndex, required GameSessionResult? currentResult, - required Map questionResults, + required Map> questionResults, required bool isAnswerSubmitted, required bool isCorrect, Duration? answerFeedbackDelay, @@ -168,8 +168,13 @@ class TestsStateManager extends StateManager { // Submit answer to session manager _gameSessionManager.submitAnswer(questionId, currentQuestion, answer); - final isCorrect = - _gameSessionManager.getQuestionResult(questionId)?.isCorrect ?? false; + // Для матричных вопросов проверяем все stages, для остальных - первый результат + final isCorrect = currentQuestion.when( + multipleChoice: (_) => _gameSessionManager.getQuestionResult(questionId)?.isCorrect ?? false, + inputLetters: (_) => _gameSessionManager.getQuestionResult(questionId)?.isCorrect ?? false, + match: (_) => _gameSessionManager.getQuestionResult(questionId)?.isCorrect ?? false, + matrix: (_) => _gameSessionManager.isMatrixQuestionCorrect(questionId), + ); // Play sound/haptics based on settings if (_settings.enableSounds) { @@ -246,8 +251,8 @@ class TestsStateManager extends StateManager { final questionId = _getQuestionId(currentQuestion); _gameSessionManager.submitAnswer(questionId, currentQuestion, answer); - final isCorrect = - _gameSessionManager.getQuestionResult(questionId)?.isCorrect ?? false; + // Матричный вопрос считается правильным, если все stages правильные + final isCorrect = _gameSessionManager.isMatrixQuestionCorrect(questionId); // Completion feedback (always positive UX) if (_settings.enableSounds) { @@ -540,9 +545,16 @@ class TestsStateManager extends StateManager { .map( (c) => MatrixCard( id: c.id, - image: c.imageUrl ?? c.image, // Use presigned URL if available + // Front side fields + image: c.image, + imageUrl: c.imageUrl, // Presigned URL for display original: c.original, translation: c.translation, + // Back side fields + originalBack: c.originalBack, + translationBack: c.translationBack, + imageBack: c.imageBack, + imageBackUrl: c.imageBackUrl, ), ) .toList(); @@ -558,13 +570,12 @@ class TestsStateManager extends StateManager { ) .toList(); - // Use stages if available, otherwise fallback to deprecated fields - final initialTargetCardId = stages.isNotEmpty - ? stages.first.targetCardId - : question.answer; - final initialTargetWord = stages.isNotEmpty - ? stages.first.targetWord - : question.word; + // Ensure stages are not empty + assert(stages.isNotEmpty, 'Matrix question must have at least one stage'); + + // Use stages for deprecated fields (for backward compatibility) + final initialTargetCardId = stages.first.targetCardId; + final initialTargetWord = stages.first.targetWord; questions.add( GameQuestion.matrix( @@ -575,7 +586,6 @@ class TestsStateManager extends StateManager { stages: stages, initialTargetCardId: initialTargetCardId, initialTargetWord: initialTargetWord, - word: question.word, text: question.text, ), ), diff --git a/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.freezed.dart b/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.freezed.dart index 26f553f..d31a73b 100644 --- a/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.freezed.dart +++ b/mnemo_cards_web_v2/lib/domain/state/tests_state_manager.freezed.dart @@ -12,11 +12,17 @@ part of 'tests_state_manager.dart'; // dart format off T _$identity(T value) => value; /// @nodoc -mixin _$TestsState { +mixin _$TestsState implements DiagnosticableTreeMixin { +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState')) + ; +} @override bool operator ==(Object other) { @@ -28,7 +34,7 @@ bool operator ==(Object other) { int get hashCode => runtimeType.hashCode; @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState()'; } @@ -134,7 +140,7 @@ return gameSessionCompleted(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function()? loading,TResult Function( List tests, String packId)? loaded,TResult Function( String message)? error,TResult Function( TestDto test, List questions)? gameSessionPreparing,TResult Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay)? gameSessionActive,TResult Function( TestDto test, GameSessionResult result)? gameSessionCompleted,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function()? loading,TResult Function( List tests, String packId)? loaded,TResult Function( String message)? error,TResult Function( TestDto test, List questions)? gameSessionPreparing,TResult Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map> questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay)? gameSessionActive,TResult Function( TestDto test, GameSessionResult result)? gameSessionCompleted,required TResult orElse(),}) {final _that = this; switch (_that) { case _Loading() when loading != null: return loading();case _Loaded() when loaded != null: @@ -160,7 +166,7 @@ return gameSessionCompleted(_that.test,_that.result);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function() loading,required TResult Function( List tests, String packId) loaded,required TResult Function( String message) error,required TResult Function( TestDto test, List questions) gameSessionPreparing,required TResult Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay) gameSessionActive,required TResult Function( TestDto test, GameSessionResult result) gameSessionCompleted,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function() loading,required TResult Function( List tests, String packId) loaded,required TResult Function( String message) error,required TResult Function( TestDto test, List questions) gameSessionPreparing,required TResult Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map> questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay) gameSessionActive,required TResult Function( TestDto test, GameSessionResult result) gameSessionCompleted,}) {final _that = this; switch (_that) { case _Loading(): return loading();case _Loaded(): @@ -185,7 +191,7 @@ return gameSessionCompleted(_that.test,_that.result);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function()? loading,TResult? Function( List tests, String packId)? loaded,TResult? Function( String message)? error,TResult? Function( TestDto test, List questions)? gameSessionPreparing,TResult? Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay)? gameSessionActive,TResult? Function( TestDto test, GameSessionResult result)? gameSessionCompleted,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? loading,TResult? Function( List tests, String packId)? loaded,TResult? Function( String message)? error,TResult? Function( TestDto test, List questions)? gameSessionPreparing,TResult? Function( TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map> questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay)? gameSessionActive,TResult? Function( TestDto test, GameSessionResult result)? gameSessionCompleted,}) {final _that = this; switch (_that) { case _Loading() when loading != null: return loading();case _Loaded() when loaded != null: @@ -204,7 +210,7 @@ return gameSessionCompleted(_that.test,_that.result);case _: /// @nodoc -class _Loading implements TestsState { +class _Loading with DiagnosticableTreeMixin implements TestsState { const _Loading(); @@ -212,6 +218,12 @@ class _Loading implements TestsState { +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.loading')) + ; +} @override bool operator ==(Object other) { @@ -223,7 +235,7 @@ bool operator ==(Object other) { int get hashCode => runtimeType.hashCode; @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.loading()'; } @@ -236,7 +248,7 @@ String toString() { /// @nodoc -class _Loaded implements TestsState { +class _Loaded with DiagnosticableTreeMixin implements TestsState { const _Loaded({required final List tests, required this.packId}): _tests = tests; @@ -256,6 +268,12 @@ class _Loaded implements TestsState { _$LoadedCopyWith<_Loaded> get copyWith => __$LoadedCopyWithImpl<_Loaded>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.loaded')) + ..add(DiagnosticsProperty('tests', tests))..add(DiagnosticsProperty('packId', packId)); +} @override bool operator ==(Object other) { @@ -267,7 +285,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tests),packId); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.loaded(tests: $tests, packId: $packId)'; } @@ -310,7 +328,7 @@ as String, /// @nodoc -class _Error implements TestsState { +class _Error with DiagnosticableTreeMixin implements TestsState { const _Error(this.message); @@ -323,6 +341,12 @@ class _Error implements TestsState { _$ErrorCopyWith<_Error> get copyWith => __$ErrorCopyWithImpl<_Error>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.error')) + ..add(DiagnosticsProperty('message', message)); +} @override bool operator ==(Object other) { @@ -334,7 +358,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,message); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.error(message: $message)'; } @@ -376,7 +400,7 @@ as String, /// @nodoc -class _GameSessionPreparing implements TestsState { +class _GameSessionPreparing with DiagnosticableTreeMixin implements TestsState { const _GameSessionPreparing({required this.test, required final List questions}): _questions = questions; @@ -396,6 +420,12 @@ class _GameSessionPreparing implements TestsState { _$GameSessionPreparingCopyWith<_GameSessionPreparing> get copyWith => __$GameSessionPreparingCopyWithImpl<_GameSessionPreparing>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.gameSessionPreparing')) + ..add(DiagnosticsProperty('test', test))..add(DiagnosticsProperty('questions', questions)); +} @override bool operator ==(Object other) { @@ -407,7 +437,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,test,const DeepCollectionEquality().hash(_questions)); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.gameSessionPreparing(test: $test, questions: $questions)'; } @@ -450,8 +480,8 @@ as List, /// @nodoc -class _GameSessionActive implements TestsState { - const _GameSessionActive({required this.test, required final List questions, required this.currentQuestionIndex, required this.currentResult, required final Map questionResults, required this.isAnswerSubmitted, required this.isCorrect, this.answerFeedbackDelay}): _questions = questions,_questionResults = questionResults; +class _GameSessionActive with DiagnosticableTreeMixin implements TestsState { + const _GameSessionActive({required this.test, required final List questions, required this.currentQuestionIndex, required this.currentResult, required final Map> questionResults, required this.isAnswerSubmitted, required this.isCorrect, this.answerFeedbackDelay}): _questions = questions,_questionResults = questionResults; final TestDto test; @@ -464,8 +494,8 @@ class _GameSessionActive implements TestsState { final int currentQuestionIndex; final GameSessionResult? currentResult; - final Map _questionResults; - Map get questionResults { + final Map> _questionResults; + Map> get questionResults { if (_questionResults is EqualUnmodifiableMapView) return _questionResults; // ignore: implicit_dynamic_type return EqualUnmodifiableMapView(_questionResults); @@ -482,6 +512,12 @@ class _GameSessionActive implements TestsState { _$GameSessionActiveCopyWith<_GameSessionActive> get copyWith => __$GameSessionActiveCopyWithImpl<_GameSessionActive>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.gameSessionActive')) + ..add(DiagnosticsProperty('test', test))..add(DiagnosticsProperty('questions', questions))..add(DiagnosticsProperty('currentQuestionIndex', currentQuestionIndex))..add(DiagnosticsProperty('currentResult', currentResult))..add(DiagnosticsProperty('questionResults', questionResults))..add(DiagnosticsProperty('isAnswerSubmitted', isAnswerSubmitted))..add(DiagnosticsProperty('isCorrect', isCorrect))..add(DiagnosticsProperty('answerFeedbackDelay', answerFeedbackDelay)); +} @override bool operator ==(Object other) { @@ -493,7 +529,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,test,const DeepCollectionEquality().hash(_questions),currentQuestionIndex,currentResult,const DeepCollectionEquality().hash(_questionResults),isAnswerSubmitted,isCorrect,answerFeedbackDelay); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.gameSessionActive(test: $test, questions: $questions, currentQuestionIndex: $currentQuestionIndex, currentResult: $currentResult, questionResults: $questionResults, isAnswerSubmitted: $isAnswerSubmitted, isCorrect: $isCorrect, answerFeedbackDelay: $answerFeedbackDelay)'; } @@ -505,7 +541,7 @@ abstract mixin class _$GameSessionActiveCopyWith<$Res> implements $TestsStateCop factory _$GameSessionActiveCopyWith(_GameSessionActive value, $Res Function(_GameSessionActive) _then) = __$GameSessionActiveCopyWithImpl; @useResult $Res call({ - TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay + TestDto test, List questions, int currentQuestionIndex, GameSessionResult? currentResult, Map> questionResults, bool isAnswerSubmitted, bool isCorrect, Duration? answerFeedbackDelay }); @@ -529,7 +565,7 @@ as TestDto,questions: null == questions ? _self._questions : questions // ignore as List,currentQuestionIndex: null == currentQuestionIndex ? _self.currentQuestionIndex : currentQuestionIndex // ignore: cast_nullable_to_non_nullable as int,currentResult: freezed == currentResult ? _self.currentResult : currentResult // ignore: cast_nullable_to_non_nullable as GameSessionResult?,questionResults: null == questionResults ? _self._questionResults : questionResults // ignore: cast_nullable_to_non_nullable -as Map,isAnswerSubmitted: null == isAnswerSubmitted ? _self.isAnswerSubmitted : isAnswerSubmitted // ignore: cast_nullable_to_non_nullable +as Map>,isAnswerSubmitted: null == isAnswerSubmitted ? _self.isAnswerSubmitted : isAnswerSubmitted // ignore: cast_nullable_to_non_nullable as bool,isCorrect: null == isCorrect ? _self.isCorrect : isCorrect // ignore: cast_nullable_to_non_nullable as bool,answerFeedbackDelay: freezed == answerFeedbackDelay ? _self.answerFeedbackDelay : answerFeedbackDelay // ignore: cast_nullable_to_non_nullable as Duration?, @@ -554,7 +590,7 @@ $GameSessionResultCopyWith<$Res>? get currentResult { /// @nodoc -class _GameSessionCompleted implements TestsState { +class _GameSessionCompleted with DiagnosticableTreeMixin implements TestsState { const _GameSessionCompleted({required this.test, required this.result}); @@ -568,6 +604,12 @@ class _GameSessionCompleted implements TestsState { _$GameSessionCompletedCopyWith<_GameSessionCompleted> get copyWith => __$GameSessionCompletedCopyWithImpl<_GameSessionCompleted>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'TestsState.gameSessionCompleted')) + ..add(DiagnosticsProperty('test', test))..add(DiagnosticsProperty('result', result)); +} @override bool operator ==(Object other) { @@ -579,7 +621,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,test,result); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'TestsState.gameSessionCompleted(test: $test, result: $result)'; } diff --git a/mnemo_cards_web_v2/lib/presentation/pages/game/game_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/game/game_page.dart index 293f07e..a9e1dcc 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/game/game_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/game/game_page.dart @@ -251,7 +251,7 @@ class _GamePageState extends State { int currentQuestionIndex, bool isAnswerSubmitted, bool isCorrect, - Map questionResults, + Map> questionResults, Duration sessionElapsed, ) { final theme = Theme.of(context); @@ -296,6 +296,7 @@ class _GamePageState extends State { currentQuestion: currentQuestionIndex, totalQuestions: questions.length, correctAnswers: questionResults.values + .expand((results) => results) .where((r) => r.isCorrect) .length, timeElapsed: sessionElapsed, @@ -889,11 +890,11 @@ class _GamePageState extends State { String? _getSelectedAnswerForMultipleChoice( MultipleChoiceQuestion question, - Map questionResults, + Map> questionResults, ) { final questionId = question.id; - final result = questionResults[questionId]; - return result?.selectedAnswer; + final results = questionResults[questionId]; + return results?.isNotEmpty == true ? results!.first.selectedAnswer : null; } Color _scoreColor(ColorScheme colorScheme, int score) { diff --git a/mnemo_cards_web_v2/lib/presentation/pages/playground/mock_data.dart b/mnemo_cards_web_v2/lib/presentation/pages/playground/mock_data.dart index 217284b..e6e8202 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/playground/mock_data.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/playground/mock_data.dart @@ -127,54 +127,72 @@ final mockMatrixQuestion = MatrixQuestion( image: 'https://via.placeholder.com/100x100?text=1', original: 'Apple', translation: 'Яблоко', + originalBack: 'Apple', + translationBack: 'Яблоко', ), MatrixCard( id: 'card2', image: 'https://via.placeholder.com/100x100?text=2', original: 'Orange', translation: 'Апельсин', + originalBack: 'Orange', + translationBack: 'Апельсин', ), MatrixCard( id: 'card3', image: 'https://via.placeholder.com/100x100?text=3', original: 'Banana', translation: 'Банан', + originalBack: 'Banana', + translationBack: 'Банан', ), MatrixCard( id: 'card4', image: 'https://via.placeholder.com/100x100?text=4', original: 'Grape', translation: 'Виноград', + originalBack: 'Grape', + translationBack: 'Виноград', ), MatrixCard( id: 'card5', image: 'https://via.placeholder.com/100x100?text=5', original: 'Cherry', translation: 'Вишня', + originalBack: 'Cherry', + translationBack: 'Вишня', ), MatrixCard( id: 'card6', image: 'https://via.placeholder.com/100x100?text=6', original: 'Strawberry', translation: 'Клубника', + originalBack: 'Strawberry', + translationBack: 'Клубника', ), MatrixCard( id: 'card7', image: 'https://via.placeholder.com/100x100?text=7', original: 'Watermelon', translation: 'Арбуз', + originalBack: 'Watermelon', + translationBack: 'Арбуз', ), MatrixCard( id: 'card8', image: 'https://via.placeholder.com/100x100?text=8', original: 'Pineapple', translation: 'Ананас', + originalBack: 'Pineapple', + translationBack: 'Ананас', ), MatrixCard( id: 'card9', image: 'https://via.placeholder.com/100x100?text=9', original: 'Mango', translation: 'Манго', + originalBack: 'Mango', + translationBack: 'Манго', ), ], stages: [ @@ -194,6 +212,6 @@ final mockMatrixQuestion = MatrixQuestion( targetAudio: null, ), ], - word: 'apple', + initialTargetWord: 'Apple', type: 'matrix', ); diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/matrix_widget.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/matrix_widget.dart index 06f17fd..941fa13 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/matrix_widget.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/matrix_widget.dart @@ -547,49 +547,7 @@ class _MatrixFlipCard extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(21.r), child: isBack - ? Transform( - alignment: Alignment.center, - transform: Matrix4.identity()..rotateY(math.pi), - child: Center( - child: Padding( - padding: EdgeInsets.all(24.w), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (card.original != null) - Text( - card.original!, - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (card.original != null && - card.translation != null) - SizedBox(height: 12.h), - if (card.translation != null) - Text( - card.translation!, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w400, - color: colorScheme.onSurface.withOpacity( - 0.5, - ), - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ) + ? _buildCardBack(card, colorScheme) : _buildCardFront(card, colorScheme), ), ), @@ -600,7 +558,9 @@ class _MatrixFlipCard extends StatelessWidget { } Widget _buildCardFront(MatrixCard card, ColorScheme colorScheme) { - final hasImage = card.image != null && card.image!.isNotEmpty; + // Front side: use only front fields + final frontImage = card.imageUrl ?? card.image; + final hasImage = frontImage != null && frontImage.isNotEmpty; final hasOriginal = card.original != null && card.original!.isNotEmpty; final hasTranslation = card.translation != null && card.translation!.isNotEmpty; @@ -648,7 +608,7 @@ class _MatrixFlipCard extends StatelessWidget { // If only image (no text), show image full size if (hasImage && !hasText) { return Image.network( - card.image!, + frontImage!, fit: BoxFit.cover, width: double.infinity, errorBuilder: (context, error, stackTrace) { @@ -701,7 +661,7 @@ class _MatrixFlipCard extends StatelessWidget { ), Expanded( child: Image.network( - card.image!, + frontImage!, fit: BoxFit.cover, width: double.infinity, errorBuilder: (context, error, stackTrace) { @@ -746,4 +706,68 @@ class _MatrixFlipCard extends StatelessWidget { ), ); } + + Widget _buildCardBack(MatrixCard card, ColorScheme colorScheme) { + // Back side: use only back fields + final backImage = card.imageBackUrl ?? card.imageBack; + final hasImage = backImage != null && backImage.isNotEmpty; + final hasOriginal = card.originalBack != null && card.originalBack!.isNotEmpty; + final hasTranslation = + card.translationBack != null && card.translationBack!.isNotEmpty; + final hasText = hasOriginal || hasTranslation; + + return Transform( + alignment: Alignment.center, + transform: Matrix4.identity()..rotateY(math.pi), + child: Center( + child: Padding( + padding: EdgeInsets.all(24.w), + child: hasImage && !hasText + ? Image.network( + backImage!, + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Icon( + Icons.image_not_supported, + color: colorScheme.onSurfaceVariant, + ), + ); + }, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (hasOriginal) + Text( + card.originalBack!, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (hasOriginal && hasTranslation) SizedBox(height: 12.h), + if (hasTranslation) + Text( + card.translationBack!, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w400, + color: colorScheme.onSurface.withOpacity(0.5), + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } }