diff --git a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart index 575e64e..e3c6b0b 100644 --- a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart @@ -258,11 +258,11 @@ class UsersApiV2 { } // Convert UserData to UserDataDto - final userDataModel = await _userRepository.getUserById( + final userModel = await _userRepository.getUserById( user.id!, withData: true, ); - if (userDataModel?.userData == null) { + if (userModel?.userData == null) { return _json({'error': 'user_data_not_found'}, statusCode: 404); } @@ -297,12 +297,7 @@ class UsersApiV2 { try { final packId = request.url.queryParameters['packId']; - final userData = user.userData; - - if (userData == null) { - return _json([]); - } - + // Рассчитать packProgress на лету из WordStatistics final allPackProgress = await _statisticsCalculator .calculateAllPackProgress(user.id!); @@ -562,7 +557,7 @@ class UsersApiV2 { return _json([]); } - final achievements = _statisticsCalculator.calculateAchievementProgress( + final achievements = _statisticsCalculator.calculateAchievemegntProgress( userData, ); return _json(achievements.map((a) => a.toJson()).toList()); diff --git a/mnemo_cards_common_backend/lib/src/models/user/user_model.dart b/mnemo_cards_common_backend/lib/src/models/user/user_model.dart index 59e080f..6dfa66c 100644 --- a/mnemo_cards_common_backend/lib/src/models/user/user_model.dart +++ b/mnemo_cards_common_backend/lib/src/models/user/user_model.dart @@ -1,9 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:mnemo_cards_common_backend/src/models/card_pack_model.dart'; -import 'package:mnemo_cards_common_backend/src/models/user/user_data_model.dart'; - -import '../subscription/user_subscription_model.dart'; part 'user_model.g.dart'; @@ -18,9 +15,7 @@ class UserModel { @JsonKey(defaultValue: []) final List purchases; final List packs; - UserDataModel? userData; - UserSubscriptionModel? subscriptionModel; - + // UserSettingsDto final String? userSettings; 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 925bb07..2452c3b 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 @@ -231,37 +231,14 @@ class GameSessionManager { if (answer is! String) return false; // Remove spaces from answer - final answerNoSpaces = answer.replaceAll(' ', ''); - final correctNoSpaces = question.correctAnswer.replaceAll(' ', ''); + final answerNoSpaces = answer.replaceAll(' ', '').toLowerCase(); + final correctNoSpaces = question.correctAnswer.replaceAll(' ', '').toLowerCase(); // If lengths don't match, answer is wrong if (answerNoSpaces.length != correctNoSpaces.length) return false; - - // Build expected answer based on template - // '_' = lowercase, '|' = uppercase - final template = question.template; - final expectedAnswer = StringBuffer(); - int answerLetterIndex = 0; - - for (int i = 0; i < template.length && answerLetterIndex < correctNoSpaces.length; i++) { - final templateChar = template[i]; - if (templateChar == '_' || templateChar == '|') { - // This is a slot - use the corresponding letter from correct answer - final correctChar = correctNoSpaces[answerLetterIndex]; - if (templateChar == '|') { - // Uppercase slot - expect uppercase - expectedAnswer.write(correctChar.toUpperCase()); - } else { - // Lowercase slot - expect lowercase - expectedAnswer.write(correctChar.toLowerCase()); - } - answerLetterIndex++; - } - // Skip other characters in template (visible letters, spaces) - } - + // Compare user answer with expected answer (case-sensitive for uppercase slots) - return answerNoSpaces == expectedAnswer.toString(); + return answerNoSpaces == question.correctAnswer; } bool _validateMatch(MatchQuestion question, dynamic answer) { 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 7460894..a57a8ea 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 @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:game_tests/game_tests.dart'; @@ -521,6 +522,20 @@ class TestsStateManager extends StateManager { } else if (question is MatrixTestQuestionBody) { final id = question.id?.toString() ?? 'q_${questions.length}'; + // Validate and log matrix question data + if (question.cards.isEmpty) { + debugPrint( + 'TestsStateManager: Warning - matrix question $id has empty cards list. ' + 'matrixSize: ${question.matrixSize}, stages: ${question.stages.length}', + ); + } else { + debugPrint( + 'TestsStateManager: Converting matrix question $id with ' + '${question.cards.length} cards, matrixSize: ${question.matrixSize}, ' + 'stages: ${question.stages.length}', + ); + } + final cards = question.cards .map( (c) => MatrixCard( diff --git a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart index 7949f1b..6f7c706 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart @@ -906,7 +906,6 @@ class _PackDetailsPageState extends State { width: width, height: height, fit: BoxFit.cover, - useCachedNetworkImage: true, maxWidthDiskCache: 2048, maxHeightDiskCache: 2048, fadeInDuration: const Duration(milliseconds: 100), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart index 5a7a2a9..9afbd70 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart @@ -724,7 +724,6 @@ class _CardSide extends StatelessWidget { card: card, packId: packId, fit: BoxFit.cover, - useCachedNetworkImage: false, errorWidget: (context, url, error) { return Container( color: packColor.withOpacity(0.1), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart index 027c218..b2f5d87 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart @@ -553,7 +553,6 @@ class _CardSide extends StatelessWidget { card: card, packId: packId, fit: BoxFit.cover, - useCachedNetworkImage: false, errorWidget: (context, url, error) { return Container( color: packColor.withOpacity(0.1), @@ -575,7 +574,6 @@ class _CardSide extends StatelessWidget { packId: packId, fit: BoxFit.contain, isBackImage: true, - useCachedNetworkImage: false, placeholder: (context, url) { return Center( child: CircularProgressIndicator( diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart b/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart index e2c4059..5348464 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart @@ -33,7 +33,6 @@ class ExpiringCardImage extends StatefulWidget { this.fadeInDuration = const Duration(milliseconds: 100), this.fadeOutDuration = const Duration(milliseconds: 100), this.isBackImage = false, - this.useCachedNetworkImage = true, }); final GameCardDto card; @@ -48,7 +47,6 @@ class ExpiringCardImage extends StatefulWidget { final Duration fadeInDuration; final Duration fadeOutDuration; final bool isBackImage; - final bool useCachedNetworkImage; @override State createState() => _ExpiringCardImageState(); @@ -191,22 +189,6 @@ class _ExpiringCardImageState extends State { ); if (mounted) { - // Clear cache for old URL before updating - if (_currentUrl != null && widget.useCachedNetworkImage) { - try { - await CachedNetworkImage.evictFromCache(_currentUrl!); - log( - 'Cleared cache for old URL: $_currentUrl', - name: 'ExpiringCardImage', - ); - } catch (e) { - log( - 'Failed to clear cache: $e', - name: 'ExpiringCardImage', - ); - } - } - setState(() { _currentUrl = newUrl; _isRefreshing = false; @@ -360,38 +342,26 @@ class _ExpiringCardImageState extends State { return _buildErrorWidget(context, '', null); } - if (widget.useCachedNetworkImage) { - // Use key based on URL to force rebuild when URL changes - return CachedNetworkImage( - key: ValueKey('cached_image_$_currentUrl'), - imageUrl: _currentUrl!, - fit: widget.fit, - width: widget.width, - height: widget.height, - maxWidthDiskCache: widget.maxWidthDiskCache, - maxHeightDiskCache: widget.maxHeightDiskCache, - fadeInDuration: widget.fadeInDuration, - fadeOutDuration: widget.fadeOutDuration, - placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(), - errorWidget: _buildErrorWidget, - // Clear cache on error to allow retry with new URL - cacheKey: _currentUrl, - ); - } else { - return Image.network( - _currentUrl!, - key: ValueKey('network_image_$_currentUrl'), - fit: widget.fit, - width: widget.width, - height: widget.height, - errorBuilder: (context, error, stackTrace) { - return _buildErrorWidget(context, _currentUrl!, error); - }, - loadingBuilder: widget.placeholder != null - ? null - : (context, child, loadingProgress) => child, - ); - } + final objectId = _getObjectId(); + // Use objectId as cacheKey so cache persists across URL refreshes + final cacheKey = objectId ?? _currentUrl; + + // Use key based on objectId to force rebuild when objectId changes + return CachedNetworkImage( + key: ValueKey('cached_image_$cacheKey'), + imageUrl: _currentUrl!, + fit: widget.fit, + width: widget.width, + height: widget.height, + maxWidthDiskCache: widget.maxWidthDiskCache, + maxHeightDiskCache: widget.maxHeightDiskCache, + fadeInDuration: widget.fadeInDuration, + fadeOutDuration: widget.fadeOutDuration, + placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(), + errorWidget: _buildErrorWidget, + // Cache by objectId (UUID) instead of URL, so cache persists when presigned URL refreshes + cacheKey: cacheKey, + ); } } 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 ba39246..0d97b5b 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 @@ -81,6 +81,19 @@ class _MatrixWidgetState extends State } void _initFromQuestion(MatrixQuestion q) { + // Validate that cards are not empty + if (q.cards.isEmpty) { + debugPrint( + 'MatrixWidget: Warning - question ${q.id} has empty cards list. ' + 'matrixSize: ${q.matrixSize}, stages: ${q.stages.length}', + ); + } else { + debugPrint( + 'MatrixWidget: Initializing question ${q.id} with ' + '${q.cards.length} cards, matrixSize: ${q.matrixSize}', + ); + } + _slots = q.cards.map((c) => c).toList(growable: false); _flipped.clear(); _correctIdsInOrder.clear(); @@ -356,10 +369,25 @@ class _MatrixWidgetState extends State Widget _buildSlot(BuildContext context, MatrixCard? card) { if (card == null) { + final colorScheme = Theme.of(context).colorScheme; return AspectRatio( aspectRatio: 1, child: DecoratedBox( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r)), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(24.r), + border: Border.all( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + child: Center( + child: Icon( + Icons.help_outline, + color: colorScheme.onSurfaceVariant.withOpacity(0.5), + size: 32.sp, + ), + ), ), ); } @@ -637,10 +665,10 @@ class _MatrixFlipCard extends StatelessWidget { } // If both image and text, show text on top and image below - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (hasText) + if (hasImage && hasText) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Padding( padding: EdgeInsets.fromLTRB(2.w, 12.h, 2.w, 8.h), child: Column( @@ -673,7 +701,6 @@ class _MatrixFlipCard extends StatelessWidget { ], ), ), - if (hasImage) Expanded( child: Image.network( card.image!, @@ -689,7 +716,36 @@ class _MatrixFlipCard extends StatelessWidget { }, ), ), - ], + ], + ); + } + + // Fallback: if no image and no text, show card ID as placeholder + return Center( + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.help_outline, + color: colorScheme.onSurfaceVariant.withOpacity(0.5), + size: 48.sp, + ), + SizedBox(height: 8.h), + Text( + card.id, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant.withOpacity(0.7), + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), ); } } diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart index 3fd2d1b..31fdd74 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart @@ -255,7 +255,6 @@ class PackCardItem extends StatelessWidget { width: cardWidth, height: cardHeight, fit: BoxFit.contain, - useCachedNetworkImage: false, placeholder: (context, url) { return Center( child: SizedBox(