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 6f5e6af..560195e 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 @@ -712,6 +712,8 @@ class AdminTestsApiV2 { uiData['template'] = questionJson['template']; if (questionJson['matrixSize'] != null) uiData['matrixSize'] = questionJson['matrixSize']; + if (questionJson['stages'] != null) + uiData['stages'] = questionJson['stages']; await _db.testDao.createTestQuestion( TestQuestionsCompanion.insert( @@ -839,6 +841,8 @@ class AdminTestsApiV2 { uiData['template'] = questionJson['template']; if (questionJson['matrixSize'] != null) uiData['matrixSize'] = questionJson['matrixSize']; + if (questionJson['stages'] != null) + uiData['stages'] = questionJson['stages']; await _db.testDao.createTestQuestion( TestQuestionsCompanion.insert( diff --git a/mnemo_cards_backend/lib/storage/minio_config.dart b/mnemo_cards_backend/lib/storage/minio_config.dart index 7426264..bd76f5b 100644 --- a/mnemo_cards_backend/lib/storage/minio_config.dart +++ b/mnemo_cards_backend/lib/storage/minio_config.dart @@ -17,7 +17,7 @@ class MinioConfig { // Presigned URL expiration (7 days) // Increased from 4 hours to prevent 403 errors on cached card data - static const int presignedUrlExpirySeconds = 7 * 24 * 60 * 60; + static const int presignedUrlExpirySeconds = 1 * 24 * 60 * 60; MinioConfig({ required this.endpoint, 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 7252539..34acc46 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,35 +194,20 @@ class MatrixQuestionGenerator implements QuestionGenerator { // Generate stages for all cards final stages = _generateStages(selected, questionType); - // Create cards based on question type + // Create cards - always include original and translation for back side display + // Front side will show only relevant fields based on question type, + // but back side needs both original and translation final cards = selected.map((c) { - if (questionType.translationCards) { - return MatrixCardDto( - id: c.id, - translation: c.translation, - // Don't include image or original for translation cards - ); - } else if (questionType.originalCards) { - return MatrixCardDto( - id: c.id, - original: c.original, - // Don't include image or translation for original cards - ); - } else if (questionType.imageCards) { - return MatrixCardDto( - id: c.id, - image: _imageIdToUrl(c.image) ?? c.image ?? c.id, - // Don't include text for image cards - ); - } else { - // Fallback: include all fields - return MatrixCardDto( - id: c.id, - image: _imageIdToUrl(c.image) ?? c.image ?? c.id, - original: c.original, - translation: c.translation, - ); - } + // Always include original and translation for back side + // Include image if available (for image-based question types) + return MatrixCardDto( + id: c.id, + image: questionType.imageCards + ? (_imageIdToUrl(c.image) ?? c.image ?? c.id) + : null, + original: c.original, + translation: c.translation, + ); }).toList(); // Get first stage for backward compatibility diff --git a/mnemo_cards_backend/lib/tests/test_manager.dart b/mnemo_cards_backend/lib/tests/test_manager.dart index edf10bf..0d4a109 100644 --- a/mnemo_cards_backend/lib/tests/test_manager.dart +++ b/mnemo_cards_backend/lib/tests/test_manager.dart @@ -780,6 +780,8 @@ class TestManager { uiData['template'] = questionJson['template']; if (questionJson['matrixSize'] != null) uiData['matrixSize'] = questionJson['matrixSize']; + if (questionJson['stages'] != null) + uiData['stages'] = questionJson['stages']; final questionCompanion = TestQuestionsCompanion.insert( testId: testId, 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 ac53e00..032aec6 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 @@ -277,28 +277,6 @@ class _GamePageState extends State { final spacingAfterQuestion = availableHeight > 700 ? 16.h : 12.h; final spacingAfterAnswer = availableHeight > 700 ? 16.h : 12.h; - // Calculate max heights for question and answer sections - // Reserve space: progress (40-100px), navigation buttons (56px), spacing - final reservedHeight = progressMode == ProgressIndicatorMode.full - ? 120.h - : progressMode == ProgressIndicatorMode.compact - ? 80.h - : 60.h; - final navigationHeight = - (_canGoPrevious(state) || - _canGoNext(state) || - _isLastQuestion(state)) - ? 72.h - : 0; - final totalSpacing = - spacingAfterProgress + spacingAfterQuestion + spacingAfterAnswer; - final availableForContent = - availableHeight - reservedHeight - navigationHeight - totalSpacing; - - // Distribute: 40% question, 60% answers (adjustable) - final questionMaxHeight = availableForContent * 0.4; - final answerMaxHeight = availableForContent * 0.6; - return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: contentWidth), @@ -333,47 +311,40 @@ class _GamePageState extends State { // Question display Flexible( flex: 2, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: questionMaxHeight, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: KeyedSubtree( - key: questionKey, - child: Material( - key: GamePage.questionCardKey, - color: colorScheme.surface, - surfaceTintColor: colorScheme.surfaceTint, - elevation: 3, - shadowColor: theme.shadowColor.withOpacity( - theme.brightness == Brightness.dark - ? 0.35 - : 0.14, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: KeyedSubtree( + key: questionKey, + child: Material( + key: GamePage.questionCardKey, + color: colorScheme.surface, + surfaceTintColor: colorScheme.surfaceTint, + elevation: 3, + shadowColor: theme.shadowColor.withOpacity( + theme.brightness == Brightness.dark + ? 0.35 + : 0.14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.r), + side: BorderSide( + color: colorScheme.outlineVariant, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.r), - side: BorderSide( - color: colorScheme.outlineVariant, - ), - ), - child: Padding( - padding: EdgeInsets.all( - isNarrow ? 14.w : 18.w, - ), - child: currentQuestion is GameQuestionMatrix - ? MatrixWidget( - question: currentQuestion.question, - maxHeight: questionMaxHeight, - ) - : QuestionDisplay( - question: currentQuestion, - onPlayAudio: - widget.questionAudioPlayback ?? - _playQuestionAudio, - maxHeight: questionMaxHeight, - ), + ), + child: Padding( + padding: EdgeInsets.all( + isNarrow ? 14.w : 18.w, ), + child: currentQuestion is GameQuestionMatrix + ? MatrixWidget( + question: currentQuestion.question, + ) + : QuestionDisplay( + question: currentQuestion, + onPlayAudio: + widget.questionAudioPlayback ?? + _playQuestionAudio, + ), ), ), ), @@ -385,53 +356,45 @@ class _GamePageState extends State { // Answer input based on question type with smooth transitions Flexible( flex: 3, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: answerMaxHeight, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0.05, 0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - ); - }, - child: Container( - key: ValueKey( - 'question_input_${currentQuestion.hashCode}', + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.05, 0), + end: Offset.zero, + ).animate(animation), + child: child, ), - child: currentQuestion.when( - multipleChoice: (q) => AnswerOptions( - question: q, - selectedAnswer: - _getSelectedAnswerForMultipleChoice( - q, - questionResults, - ), - onAnswerSelected: _onAnswerSelected, - isAnswerSubmitted: isAnswerSubmitted, - isCorrect: isCorrect, - maxHeight: answerMaxHeight, - ), - inputLetters: (q) => InputLettersWidget( - question: q, - maxHeight: answerMaxHeight, - ), - match: (q) => MatchWidget( - question: q, - maxHeight: answerMaxHeight, - ), - matrix: (q) => const SizedBox.shrink(), + ); + }, + child: Container( + key: ValueKey( + 'question_input_${currentQuestion.hashCode}', + ), + child: currentQuestion.when( + multipleChoice: (q) => AnswerOptions( + question: q, + selectedAnswer: + _getSelectedAnswerForMultipleChoice( + q, + questionResults, + ), + onAnswerSelected: _onAnswerSelected, + isAnswerSubmitted: isAnswerSubmitted, + isCorrect: isCorrect, ), + inputLetters: (q) => InputLettersWidget( + question: q, + ), + match: (q) => MatchWidget( + question: q, + ), + matrix: (q) => const SizedBox.shrink(), ), ), ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart index 2540c40..f2584b0 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart @@ -11,7 +11,6 @@ class AnswerOptions extends StatelessWidget { required this.isAnswerSubmitted, required this.isCorrect, this.enabled = true, - this.maxHeight, super.key, }); @@ -21,7 +20,6 @@ class AnswerOptions extends StatelessWidget { final bool isAnswerSubmitted; final bool isCorrect; final bool enabled; - final double? maxHeight; void _onAnswerSelected(BuildContext context, String option) { // Note: Sound service access would be implemented through proper DI injection @@ -48,22 +46,11 @@ class AnswerOptions extends StatelessWidget { hasOptionItems && question.optionItems.any((item) => item.image != null); - // Adapt aspect ratio based on maxHeight - double childAspectRatio; - if (hasImages) { - childAspectRatio = maxHeight != null && maxHeight! < 300 ? 1.0 : 1.1; - } else { - childAspectRatio = maxHeight != null && maxHeight! < 300 ? 3.0 : 4.0; - } + // Fixed aspect ratios for consistent layout + final childAspectRatio = hasImages ? 1.1 : 4.0; + final spacing = 12.0; - // Adapt spacing based on available height - final spacing = maxHeight != null && maxHeight! < 300 ? 8.0 : 12.0; - - return ConstrainedBox( - constraints: maxHeight != null - ? BoxConstraints(maxHeight: maxHeight!) - : const BoxConstraints(), - child: GridView.builder( + return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( @@ -82,8 +69,7 @@ class AnswerOptions extends StatelessWidget { return _buildAnswerOption(context, option); } }, - ), - ); + ); }, ); } @@ -186,7 +172,7 @@ class AnswerOptions extends StatelessWidget { splashColor: borderColor?.withOpacity(0.1), child: AnimatedContainer( duration: const Duration(milliseconds: 300), - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border.all( color: @@ -268,6 +254,7 @@ class AnswerOptions extends StatelessWidget { textColor: textColor, isSelected: isSelected, isCorrectOption: isCorrectOption, + isAnswerSubmitted: isAnswerSubmitted, ), ), ], @@ -288,6 +275,7 @@ class AnswerOptions extends StatelessWidget { Color? textColor, required bool isSelected, required bool isCorrectOption, + required bool isAnswerSubmitted, }) { // If we have an image, show it (with optional text below) if (image != null) { @@ -345,7 +333,7 @@ class AnswerOptions extends StatelessWidget { duration: const Duration(milliseconds: 300), style: Theme.of(context).textTheme.bodyLarge!.copyWith( color: textColor, - fontWeight: isSelected || isCorrectOption + fontWeight: isSelected || (isAnswerSubmitted && isCorrectOption) ? FontWeight.w600 : FontWeight.normal, fontSize: isSelected ? 17 : 16, 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 cadc673..eb5d3a4 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 @@ -7,10 +7,9 @@ import '../../../domain/models/game_question.dart'; /// Widget for input letters questions - user types letters to fill in blanks class InputLettersWidget extends StatefulWidget { - const InputLettersWidget({required this.question, this.maxHeight, super.key}); + const InputLettersWidget({required this.question, super.key}); final InputLettersQuestion question; - final double? maxHeight; @override State createState() => _InputLettersWidgetState(); @@ -72,71 +71,63 @@ class _InputLettersWidgetState extends State { @override Widget build(BuildContext context) { - // Adapt spacing and padding based on maxHeight - final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; - final containerPadding = isCompact ? 12.w : 16.w; - final containerMargin = isCompact ? 12.h : 16.h; - final spacingAfterTitle = isCompact ? 8.h : 12.h; - final spacingAfterWord = isCompact ? 8.h : 12.h; - final spacingAfterGrid = isCompact ? 12.h : 16.h; - final showFillInstruction = !isCompact; // Hide on very compact screens + // Fixed spacing and padding for consistent layout + final containerPadding = 16.w; + final containerMargin = 16.h; + final spacingAfterTitle = 12.h; + final spacingAfterWord = 12.h; + final spacingAfterGrid = 16.h; return LayoutBuilder( builder: (context, constraints) { - return ConstrainedBox( - constraints: widget.maxHeight != null - ? BoxConstraints(maxHeight: widget.maxHeight!) - : const BoxConstraints(), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - // Display the template with current input - Flexible( - child: Container( - padding: EdgeInsets.all(containerPadding), - margin: EdgeInsets.only(bottom: containerMargin), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Display the template with current input + Flexible( + child: Container( + padding: EdgeInsets.all(containerPadding), + margin: EdgeInsets.only(bottom: containerMargin), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Fill in the blanks:', + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + if (widget.question.word.isNotEmpty) + SizedBox(height: spacingAfterTitle), + if (widget.question.word.isNotEmpty) ...[ + Text( + widget.question.word, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + textAlign: TextAlign.center, ), ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (showFillInstruction) - Text( - 'Fill in the blanks:', - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - ), - if (showFillInstruction && - widget.question.word.isNotEmpty) - SizedBox(height: spacingAfterTitle), - if (widget.question.word.isNotEmpty) ...[ - Text( - widget.question.word, - style: Theme.of(context).textTheme.titleLarge - ?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - ], - SizedBox(height: spacingAfterWord), - _buildTemplateDisplay(), - ], - ), + SizedBox(height: spacingAfterWord), + _buildTemplateDisplay(), + ], ), ), + ), // Buttons with images or text (if available) if (widget.question.buttons.isNotEmpty) ...[ @@ -208,12 +199,11 @@ class _InputLettersWidgetState extends State { final parts = []; int inputIndex = 0; - // Adapt sizes based on maxHeight - final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; - final cellWidth = isCompact ? 24.w : 28.w; - final cellHeight = isCompact ? 36.h : 48.h; - final cellFontSize = isCompact ? 16.sp : 20.sp; - final blankCellWidth = isCompact ? 28.w : 32.w; + // Fixed cell sizes for consistent layout + final cellWidth = 28.w; + final cellHeight = 48.h; + final cellFontSize = 20.sp; + final blankCellWidth = 32.w; for (int i = 0; i < template.length; i++) { final char = template[i]; @@ -283,11 +273,8 @@ class _InputLettersWidgetState extends State { Widget _buildButtonsGrid(BoxConstraints constraints) { final hasImages = widget.question.buttons.any((b) => b.image != null); final crossAxisCount = constraints.maxWidth > 600 ? 4 : 3; - final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; - final childAspectRatio = hasImages - ? (isCompact ? 1.0 : 1.2) - : (isCompact ? 2.0 : 2.0); - final spacing = isCompact ? 8.0 : 12.0; + final childAspectRatio = hasImages ? 1.2 : 2.0; + final spacing = 12.0; return GridView.builder( shrinkWrap: true, diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/match_widget.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/match_widget.dart index 1f04097..26b63fa 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/match_widget.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/match_widget.dart @@ -7,10 +7,9 @@ import '../../../domain/models/game_question.dart'; /// Widget for match questions - user connects items from two columns class MatchWidget extends StatefulWidget { - const MatchWidget({required this.question, this.maxHeight, super.key}); + const MatchWidget({required this.question, super.key}); final MatchQuestion question; - final double? maxHeight; @override State createState() => _MatchWidgetState(); @@ -23,140 +22,130 @@ class _MatchWidgetState extends State { @override Widget build(BuildContext context) { - // Adapt spacing and padding based on maxHeight - final isCompact = widget.maxHeight != null && widget.maxHeight! < 500; - final instructionPadding = isCompact ? 12.w : 16.w; - final instructionMargin = isCompact ? 12.h : 16.h; - final connectionPadding = isCompact ? 12.w : 16.w; - final connectionMargin = isCompact ? 12.h : 16.h; - final spacingAfterColumns = isCompact ? 12.h : 16.h; - final instructionText = isCompact - ? 'Connect matching items' - : 'Connect the matching items by tapping them in order'; + // Fixed spacing and padding for consistent layout + final instructionPadding = 16.w; + final instructionMargin = 16.h; + final connectionPadding = 16.w; + final connectionMargin = 16.h; + final spacingAfterColumns = 16.h; return LayoutBuilder( builder: (context, constraints) { final isWideScreen = constraints.maxWidth > 600; - return ConstrainedBox( - constraints: widget.maxHeight != null - ? BoxConstraints(maxHeight: widget.maxHeight!) - : const BoxConstraints(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Instructions + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Instructions + Container( + padding: EdgeInsets.all(instructionPadding), + margin: EdgeInsets.only(bottom: instructionMargin), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + 'Connect the matching items by tapping them in order', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ), + + // Connection display + if (_connections.isNotEmpty) ...[ Container( - padding: EdgeInsets.all(instructionPadding), - margin: EdgeInsets.only(bottom: instructionMargin), + padding: EdgeInsets.all(connectionPadding), + margin: EdgeInsets.only(bottom: connectionMargin), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Connected: ${_connections.length}/${widget.question.correctPairs.length}', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), + SizedBox(height: 8.h), + ..._buildConnectionDisplay(), ], ), - child: Text( - instructionText, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), ), + ], - // Connection display - if (_connections.isNotEmpty) ...[ - Container( - padding: EdgeInsets.all(connectionPadding), - margin: EdgeInsets.only(bottom: connectionMargin), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(12.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Connected: ${_connections.length}/${widget.question.correctPairs.length}', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - if (!isCompact) ...[ - SizedBox(height: 8.h), - ..._buildConnectionDisplay(), + // Two columns layout + Flexible( + child: isWideScreen + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildColumn( + widget.question.leftItems, + isLeft: true, + ), + ), + SizedBox(width: 24.w), + Expanded( + child: _buildColumn( + widget.question.rightItems, + isLeft: false, + ), + ), ], - ], - ), - ), - ], - - // Two columns layout - Flexible( - child: isWideScreen - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: _buildColumn( - widget.question.leftItems, - isLeft: true, - ), + ) + : Column( + children: [ + Text( + 'Left Column', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 12.h), + Flexible( + child: _buildColumn( + widget.question.leftItems, + isLeft: true, ), - SizedBox(width: isCompact ? 16.w : 24.w), - Expanded( - child: _buildColumn( - widget.question.rightItems, - isLeft: false, - ), + ), + SizedBox(height: 24.h), + Text( + 'Right Column', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.secondary, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 12.h), + Flexible( + child: _buildColumn( + widget.question.rightItems, + isLeft: false, ), - ], - ) - : Column( - children: [ - Text( - 'Left Column', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: isCompact ? 8.h : 12.h), - Flexible( - child: _buildColumn( - widget.question.leftItems, - isLeft: true, - ), - ), - SizedBox(height: isCompact ? 16.h : 24.h), - Text( - 'Right Column', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of( - context, - ).colorScheme.secondary, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: isCompact ? 8.h : 12.h), - Flexible( - child: _buildColumn( - widget.question.rightItems, - isLeft: false, - ), - ), - ], - ), - ), + ), + ], + ), + ), SizedBox(height: spacingAfterColumns), @@ -178,18 +167,16 @@ class _MatchWidgetState extends State { } Widget _buildColumn(List items, {required bool isLeft}) { - final isCompact = widget.maxHeight != null && widget.maxHeight! < 500; - final itemMargin = isCompact ? 4.h : 6.h; - final itemPadding = isCompact ? 8.w : 12.w; - final imageSize = isCompact ? 32.0 : 40.0; - final fontSize = isCompact ? 14.sp : 16.sp; - final iconSize = isCompact ? 18.sp : 20.sp; + // Fixed sizes for consistent layout + final itemMargin = 6.h; + final itemPadding = 12.w; + final imageSize = 40.0; + final fontSize = 16.sp; + final iconSize = 20.sp; return ListView.builder( itemCount: items.length, - itemExtent: isCompact - ? 52.0 - : 64.0, // Fixed item height for better performance + itemExtent: 64.0, // Fixed item height for better performance itemBuilder: (context, index) { final item = items[index]; final isSelected = isLeft @@ -240,7 +227,7 @@ class _MatchWidgetState extends State { ), ), ), - SizedBox(width: isCompact ? 8.w : 12.w), + SizedBox(width: 12.w), ], Expanded( child: Text( 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 a44b110..ee5b9d2 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 @@ -21,14 +21,12 @@ class MatrixWidget extends StatefulWidget { required this.question, this.onWrongAttempt, this.onCompleted, - this.maxHeight, super.key, }); final MatrixQuestion question; final Future Function()? onWrongAttempt; final Future Function(MatrixImageSelectAnswer answer)? onCompleted; - final double? maxHeight; @override State createState() => _MatrixWidgetState(); @@ -175,64 +173,36 @@ class _MatrixWidgetState extends State ...List.filled(total - _slots.length, null), ]; - // Calculate available height for grid (reserve space for target container) - // Target container: ~100-120px (compact) to 140-160px (full) - final targetContainerHeight = - widget.maxHeight != null && widget.maxHeight! < 350 ? 100.0 : 140.0; - final spacingBetween = widget.maxHeight != null && widget.maxHeight! < 350 - ? 12.h - : 16.h; + // Fixed spacing values for consistent layout + final spacingBetween = 16.h; + final cellSpacing = 10.0; + final targetPaddingH = 20.w; + final targetPaddingV = 16.h; - final gridMaxHeight = widget.maxHeight != null - ? widget.maxHeight! - targetContainerHeight - spacingBetween - : null; - - // Adapt spacing for grid cells - final cellSpacing = widget.maxHeight != null && widget.maxHeight! < 350 - ? 6.0 - : 10.0; - - // Adapt padding for target container - final targetPaddingH = widget.maxHeight != null && widget.maxHeight! < 350 - ? 16.w - : 20.w; - final targetPaddingV = widget.maxHeight != null && widget.maxHeight! < 350 - ? 12.h - : 16.h; - - return ConstrainedBox( - constraints: widget.maxHeight != null - ? BoxConstraints(maxHeight: widget.maxHeight!) - : const BoxConstraints(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: ConstrainedBox( - constraints: gridMaxHeight != null - ? BoxConstraints(maxHeight: gridMaxHeight) - : const BoxConstraints(), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: size, - crossAxisSpacing: cellSpacing, - mainAxisSpacing: cellSpacing, - childAspectRatio: 1.0, - ), - itemCount: total, - itemBuilder: (context, index) { - final card = slots[index]; - return _buildSlot(context, card); - }, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: size, + crossAxisSpacing: cellSpacing, + mainAxisSpacing: cellSpacing, + childAspectRatio: 1.0, ), + itemCount: total, + itemBuilder: (context, index) { + final card = slots[index]; + return _buildSlot(context, card); + }, ), - SizedBox(height: spacingBetween), - Container( - padding: EdgeInsets.symmetric( + ), + SizedBox(height: spacingBetween), + Container( + padding: EdgeInsets.symmetric( horizontal: targetPaddingH, vertical: targetPaddingV, ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/question_display.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/question_display.dart index 903761d..b92dc68 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/question_display.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/question_display.dart @@ -10,13 +10,11 @@ class QuestionDisplay extends StatelessWidget { const QuestionDisplay({ required this.question, this.onPlayAudio, - this.maxHeight, super.key, }); final GameQuestion question; final QuestionAudioPlayback? onPlayAudio; - final double? maxHeight; @override Widget build(BuildContext context) { @@ -78,93 +76,65 @@ class QuestionDisplay extends StatelessWidget { }) { final theme = Theme.of(context).textTheme; - // Calculate available height for image (30-40% of maxHeight, min 120, max 250) - double? imageMaxHeight; - if (maxHeight != null) { - final imageHeight = maxHeight! * 0.35; - imageMaxHeight = imageHeight.clamp(120.0, 250.0); - } else { - imageMaxHeight = 200.h; - } + // Fixed spacing values for consistent layout + final spacingAfterImage = 12.h; + final spacingAfterText = 8.h; - // Calculate spacing based on available height - final spacingAfterImage = maxHeight != null && maxHeight! < 300 - ? 8.h - : 12.h; - final spacingAfterText = maxHeight != null && maxHeight! < 300 ? 6.h : 8.h; - - // Determine max lines for text based on available height - final maxLines = maxHeight != null && maxHeight! < 250 ? 3 : 5; - - // Adjust font size slightly on very small screens - final fontSize = maxHeight != null && maxHeight! < 200 ? 18.sp : 20.sp; - - return ConstrainedBox( - constraints: maxHeight != null - ? BoxConstraints(maxHeight: maxHeight!) - : const BoxConstraints(), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - // Image display - if (image != null) ...[ - Flexible( - child: Container( - constraints: BoxConstraints( - maxHeight: imageMaxHeight, - maxWidth: double.infinity, - ), - child: Image.network( - image, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 120.h, - width: 120.w, - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8.r), - ), - child: Icon( - Icons.image_not_supported, - size: 48.sp, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - }, - ), - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Image display + if (image != null) ...[ + Flexible( + child: Image.network( + image, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 120.h, + width: 120.w, + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8.r), + ), + child: Icon( + Icons.image_not_supported, + size: 48.sp, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + }, ), - SizedBox(height: spacingAfterImage), - ], - - // Text display - if (text.isNotEmpty) ...[ - Flexible( - child: Text( - text, - style: theme.headlineSmall?.copyWith( - fontSize: fontSize, - height: 1.4, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - maxLines: maxLines, - overflow: TextOverflow.fade, - ), - ), - ], - - // Audio button - if (audio != null) ...[ - SizedBox(height: spacingAfterText), - _QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio), - ], + ), + SizedBox(height: spacingAfterImage), ], - ), + + // Text display + if (text.isNotEmpty) ...[ + Flexible( + child: Text( + text, + style: theme.headlineSmall?.copyWith( + fontSize: 20.sp, + height: 1.4, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + maxLines: 5, + overflow: TextOverflow.fade, + ), + ), + ], + + // Audio button + if (audio != null) ...[ + SizedBox(height: spacingAfterText), + _QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio), + ], + ], ); } } diff --git a/mnemo_cards_web_v2/web/index.html b/mnemo_cards_web_v2/web/index.html index cbf78a0..f49db89 100644 --- a/mnemo_cards_web_v2/web/index.html +++ b/mnemo_cards_web_v2/web/index.html @@ -37,9 +37,19 @@ - + + + + +