From 9c2456f685d02b8e8c1ad818e492f8abd107ac8a Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 20 Dec 2025 02:37:34 +0300 Subject: [PATCH] fixes --- .../lib/api/v2/admin_cards_api_v2.dart | 15 +- .../lib/api/v2/admin_tests_api_v2.dart | 2 +- .../lib/tests/test_manager.dart | 144 +++++++++--------- .../lib/domain/state/tests_state_manager.dart | 14 ++ .../presentation/pages/game/game_page.dart | 5 + .../widgets/card_favorite_button.dart | 4 - .../lib/presentation/widgets/card_viewer.dart | 1 + .../widgets/game/input_letters_widget.dart | 31 +++- .../presentation/widgets/pack_card_item.dart | 11 -- 9 files changed, 133 insertions(+), 94 deletions(-) diff --git a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart index eddcd39..7fe3fdb 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart @@ -455,7 +455,18 @@ class AdminCardsApiV2 { updatedAt: PgDateTime(DateTime.now()), ); await _db.packDao.updateCard(updated); - + + // Обновить связь с паком + if (requestDto.packId != null && requestDto.packId!.isNotEmpty) { + // Сначала удалить старую связь + await _db.packDao.removeCardFromPack(updated.id, existing.packId ?? ''); + // Затем добавить новую + await _db.packDao.addCardToPack(cardId: updated.id, packId: requestDto.packId!); + } else { + // Если packId не указан или пустой, удалить связь + await _db.packDao.removeCardFromPack(updated.id, existing.packId ?? ''); + } + // Получить паки для карточки final packs = await _db.packDao.getPacksForCard(updated.id); final packId = packs.isNotEmpty ? packs.first.id : null; @@ -501,7 +512,7 @@ class AdminCardsApiV2 { final cardId = await _db.packDao.createCard(companion); // Если передан packId, создать связь через CardPackCards - if (requestDto.packId != null) { + if (requestDto.packId != null && requestDto.packId!.isNotEmpty) { await _db.packDao.addCardToPack(cardId: cardId, packId: requestDto.packId!); } diff --git a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart index c966765..a2c2d00 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart @@ -56,7 +56,7 @@ class AdminTestsApiV2 { final cardId = await _db.packDao.createCard(companion); // If packId is provided, link card to pack - if (packId != null) { + if (packId != null && packId.isNotEmpty) { await _db.packDao.addCardToPack(cardId: cardId, packId: packId); } diff --git a/mnemo_cards_backend/lib/tests/test_manager.dart b/mnemo_cards_backend/lib/tests/test_manager.dart index d73aa7e..9d6c1e5 100644 --- a/mnemo_cards_backend/lib/tests/test_manager.dart +++ b/mnemo_cards_backend/lib/tests/test_manager.dart @@ -553,88 +553,92 @@ class TestManager { final tests = await _db.testDao.getTestsByPackId(packId); - // Helper function to convert image ID to URL - String? _convertImageToUrl(String? imageValue, String? packId) { - if (imageValue == null || packId == null) return imageValue; - - // If it's already a proper URL, return as is - if (imageValue.startsWith('http://') || - imageValue.startsWith('https://') || - (imageValue.startsWith('/api/') && imageValue.contains('/cards/') && imageValue.endsWith('/image'))) { - return imageValue; - } - - // If it looks like a UUID (card ID), convert to URL - if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) { - return '/api/v2/packs/$packId/cards/$imageValue/image'; - } - - // Otherwise, assume it's already a card ID and convert - return '/api/v2/packs/$packId/cards/$imageValue/image'; - } - final testDtos = []; for (final test in tests) { final questions = await _db.testDao.getTestQuestions(test.id); final statistics = await _testStatisticsDto(user.id!, test.id); - final questionsList = questions.map((q) { - // Build question JSON from separate fields - final questionJson = { - 'questionType': q.questionType, - 'id': q.id, - 'word': q.word, - }; - - // Parse options (JSON array of buttons) - List buttons = []; - try { - buttons = json.decode(q.options) as List; - } catch (e) { - buttons = []; - } - - // Convert button images to URLs - final buttonsWithUrls = buttons.map((button) { - if (button is Map && button['image'] != null) { - final buttonMap = Map.from(button); - buttonMap['image'] = _convertImageToUrl( - buttonMap['image'] as String?, - packId, - ); - return buttonMap; + final questionsList = await Future.wait( + questions.map((q) async { + // Build question JSON from separate fields + final questionJson = { + 'questionType': q.questionType, + 'id': q.id, + 'word': q.word, + }; + + // Parse options (JSON array of buttons) + List buttons = []; + try { + buttons = json.decode(q.options) as List; + } catch (e) { + buttons = []; } - return button; - }).toList(); - - questionJson['buttons'] = buttonsWithUrls; - - // Add answer - questionJson['answer'] = q.answer; - - // Parse uiData (image, text, audio, template) - try { - final uiData = json.decode(q.uiData) as Map; - // Convert question image to URL - if (uiData['image'] != null) { - uiData['image'] = _convertImageToUrl( - uiData['image'] as String?, - packId, - ); + + // Convert button images to URLs (works for both TestButtonDto and matrix cards) + // Add imageUrl while keeping image (objectId) for admin + final updatedButtons = await Future.wait( + buttons.map((button) async { + if (button is Map && button['image'] != null) { + 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; + } + return buttonMap; + } + return button; + }), + ); + + questionJson['buttons'] = updatedButtons; + + // Add answer + questionJson['answer'] = q.answer; + + // Parse uiData (image, text, audio, template) + try { + final uiData = json.decode(q.uiData) as Map; + // Convert question image to URL + if (uiData['image'] != null) { + final imageValue = uiData['image']?.toString(); + final imageUrl = await _imageValueToApiUrl( + imageValue, + packId: packId, + ); + if (imageUrl != null) { + uiData['imageUrl'] = imageUrl; + } + } + questionJson.addAll(uiData); + } catch (e) { + // If uiData is empty or invalid, ignore } - questionJson.addAll(uiData); - } catch (e) { - // If uiData is empty or invalid, ignore - } - - return AbstractTestQuestion.fromJson(questionJson); - }).toList(); + + return AbstractTestQuestion.fromJson(questionJson); + }), + ); + + final normalizedCover = + await _normalizeImageValueForDb(test.cover, packId: packId); + final coverUrl = await _imageValueToApiUrl( + normalizedCover, + packId: packId, + ); testDtos.add(TestDto( id: test.id.toString(), name: test.name, color: test.color, - cover: test.cover, + cover: normalizedCover, // Object ID (for admin) + coverUrl: coverUrl, // Presigned URL (for display) version: test.version ?? '1.0', time: test.time, timeSubtitle: test.timeSubtitle, 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 b246f9b..5d8822f 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 @@ -295,6 +295,20 @@ class TestsStateManager extends StateManager { if (currentState is! _GameSessionActive || _currentTest == null) return; try { + // Check if current question has been answered + final currentQuestion = currentState.questions[currentState.currentQuestionIndex]; + final questionId = _getQuestionId(currentQuestion); + final existingResult = _gameSessionManager.getQuestionResult(questionId); + + // If current question hasn't been answered, save it as unanswered + if (existingResult == null || existingResult.answeredAt == null) { + log( + 'Current question not answered, saving as unanswered before completion', + name: 'TestsStateManager', + ); + _gameSessionManager.submitAnswer(questionId, currentQuestion, ''); + } + final result = _gameSessionManager.completeSession(_currentTest!.id!); // Play completion sound 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 369c20e..6783d60 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 @@ -544,10 +544,15 @@ class _GamePageState extends State { Future _finishGame() async { + log('Finish button pressed', name: 'GamePage'); final appScope = ScopeProvider.of(context, listen: false); final userScope = appScope?.userScopeHolder.scope; if (userScope != null) { + log('Completing game session...', name: 'GamePage'); await userScope.testsModule.testsStateManager.completeGameSession(); + log('Game session completion called', name: 'GamePage'); + } else { + log('No user scope available when finishing game', name: 'GamePage'); } } diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_favorite_button.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_favorite_button.dart index 5d5960b..c8d3c28 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_favorite_button.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_favorite_button.dart @@ -48,10 +48,6 @@ class CardFavoriteButton extends StatelessWidget { ), icon: Container( padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: colorScheme.surface.withOpacity(0.9), - shape: BoxShape.circle, - ), child: Icon( isFavorite ? Icons.favorite : Icons.favorite_border, color: isFavorite ? Colors.red : colorScheme.onSurface, 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 6cc24a7..9f9dd15 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart @@ -432,6 +432,7 @@ class _CardSide extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ // Верхняя секция: Original + Translation + Voice Controls Container( diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/input_letters_widget.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/input_letters_widget.dart index ef98374..42aa008 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/input_letters_widget.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/input_letters_widget.dart @@ -102,6 +102,17 @@ class _InputLettersWidgetState extends State { ), textAlign: TextAlign.center, ), + if (widget.question.word.isNotEmpty) ...[ + SizedBox(height: 16.h), + Text( + widget.question.word, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + ], SizedBox(height: 16.h), _buildTemplateDisplay(), ], @@ -166,10 +177,9 @@ class _InputLettersWidgetState extends State { final template = widget.question.template; final currentInput = _currentAnswer; - return Wrap( - alignment: WrapAlignment.center, - spacing: 8.w, - runSpacing: 8.h, + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: _buildTemplateParts(template, currentInput), ); } @@ -188,8 +198,9 @@ class _InputLettersWidgetState extends State { parts.add( Container( - width: 40.w, + width: 32.w, height: 48.h, + margin: EdgeInsets.symmetric(horizontal: 2.w), decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.primary, @@ -207,16 +218,23 @@ class _InputLettersWidgetState extends State { fontSize: 20.sp, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, + fontFeatures: const [FontFeature.tabularFigures()], ), ), ), ); + } else if (char == ' ') { + // Space character - add small spacing + parts.add( + SizedBox(width: 8.w), + ); } else { // This is a regular character parts.add( Container( - width: 32.w, + width: 28.w, height: 48.h, + margin: EdgeInsets.symmetric(horizontal: 2.w), alignment: Alignment.center, child: Text( char, @@ -224,6 +242,7 @@ class _InputLettersWidgetState extends State { fontSize: 20.sp, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface, + fontFeatures: const [FontFeature.tabularFigures()], ), ), ), 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 51a594f..b64686b 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 @@ -59,17 +59,6 @@ class PackCardItem extends StatelessWidget { child: Container( key: ValueKey('$favoriteRootKeyPrefix${card.id}'), padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: colorScheme.surface.withOpacity(0.9), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: colorScheme.shadow.withOpacity(0.2), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), child: Icon( isFavorite ? Icons.favorite : Icons.favorite_border, size: 14,