ads
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-20 22:11:07 +03:00
parent 0f49280805
commit dc01b02508
11 changed files with 368 additions and 502 deletions

View file

@ -712,6 +712,8 @@ class AdminTestsApiV2 {
uiData['template'] = questionJson['template']; uiData['template'] = questionJson['template'];
if (questionJson['matrixSize'] != null) if (questionJson['matrixSize'] != null)
uiData['matrixSize'] = questionJson['matrixSize']; uiData['matrixSize'] = questionJson['matrixSize'];
if (questionJson['stages'] != null)
uiData['stages'] = questionJson['stages'];
await _db.testDao.createTestQuestion( await _db.testDao.createTestQuestion(
TestQuestionsCompanion.insert( TestQuestionsCompanion.insert(
@ -839,6 +841,8 @@ class AdminTestsApiV2 {
uiData['template'] = questionJson['template']; uiData['template'] = questionJson['template'];
if (questionJson['matrixSize'] != null) if (questionJson['matrixSize'] != null)
uiData['matrixSize'] = questionJson['matrixSize']; uiData['matrixSize'] = questionJson['matrixSize'];
if (questionJson['stages'] != null)
uiData['stages'] = questionJson['stages'];
await _db.testDao.createTestQuestion( await _db.testDao.createTestQuestion(
TestQuestionsCompanion.insert( TestQuestionsCompanion.insert(

View file

@ -17,7 +17,7 @@ class MinioConfig {
// Presigned URL expiration (7 days) // Presigned URL expiration (7 days)
// Increased from 4 hours to prevent 403 errors on cached card data // 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({ MinioConfig({
required this.endpoint, required this.endpoint,

View file

@ -194,35 +194,20 @@ class MatrixQuestionGenerator implements QuestionGenerator {
// Generate stages for all cards // Generate stages for all cards
final stages = _generateStages(selected, questionType); 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) { final cards = selected.map((c) {
if (questionType.translationCards) { // Always include original and translation for back side
// Include image if available (for image-based question types)
return MatrixCardDto( return MatrixCardDto(
id: c.id, id: c.id,
translation: c.translation, image: questionType.imageCards
// Don't include image or original for translation cards ? (_imageIdToUrl(c.image) ?? c.image ?? c.id)
); : null,
} 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, original: c.original,
translation: c.translation, translation: c.translation,
); );
}
}).toList(); }).toList();
// Get first stage for backward compatibility // Get first stage for backward compatibility

View file

@ -780,6 +780,8 @@ class TestManager {
uiData['template'] = questionJson['template']; uiData['template'] = questionJson['template'];
if (questionJson['matrixSize'] != null) if (questionJson['matrixSize'] != null)
uiData['matrixSize'] = questionJson['matrixSize']; uiData['matrixSize'] = questionJson['matrixSize'];
if (questionJson['stages'] != null)
uiData['stages'] = questionJson['stages'];
final questionCompanion = TestQuestionsCompanion.insert( final questionCompanion = TestQuestionsCompanion.insert(
testId: testId, testId: testId,

View file

@ -277,28 +277,6 @@ class _GamePageState extends State<GamePage> {
final spacingAfterQuestion = availableHeight > 700 ? 16.h : 12.h; final spacingAfterQuestion = availableHeight > 700 ? 16.h : 12.h;
final spacingAfterAnswer = 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( return Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: contentWidth), constraints: BoxConstraints(maxWidth: contentWidth),
@ -333,10 +311,6 @@ class _GamePageState extends State<GamePage> {
// Question display // Question display
Flexible( Flexible(
flex: 2, flex: 2,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: questionMaxHeight,
),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: KeyedSubtree( child: KeyedSubtree(
@ -364,15 +338,12 @@ class _GamePageState extends State<GamePage> {
child: currentQuestion is GameQuestionMatrix child: currentQuestion is GameQuestionMatrix
? MatrixWidget( ? MatrixWidget(
question: currentQuestion.question, question: currentQuestion.question,
maxHeight: questionMaxHeight,
) )
: QuestionDisplay( : QuestionDisplay(
question: currentQuestion, question: currentQuestion,
onPlayAudio: onPlayAudio:
widget.questionAudioPlayback ?? widget.questionAudioPlayback ??
_playQuestionAudio, _playQuestionAudio,
maxHeight: questionMaxHeight,
),
), ),
), ),
), ),
@ -385,10 +356,6 @@ class _GamePageState extends State<GamePage> {
// Answer input based on question type with smooth transitions // Answer input based on question type with smooth transitions
Flexible( Flexible(
flex: 3, flex: 3,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: answerMaxHeight,
),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
switchInCurve: Curves.easeInOut, switchInCurve: Curves.easeInOut,
@ -420,22 +387,18 @@ class _GamePageState extends State<GamePage> {
onAnswerSelected: _onAnswerSelected, onAnswerSelected: _onAnswerSelected,
isAnswerSubmitted: isAnswerSubmitted, isAnswerSubmitted: isAnswerSubmitted,
isCorrect: isCorrect, isCorrect: isCorrect,
maxHeight: answerMaxHeight,
), ),
inputLetters: (q) => InputLettersWidget( inputLetters: (q) => InputLettersWidget(
question: q, question: q,
maxHeight: answerMaxHeight,
), ),
match: (q) => MatchWidget( match: (q) => MatchWidget(
question: q, question: q,
maxHeight: answerMaxHeight,
), ),
matrix: (q) => const SizedBox.shrink(), matrix: (q) => const SizedBox.shrink(),
), ),
), ),
), ),
), ),
),
// Navigation buttons (show when navigation is possible) // Navigation buttons (show when navigation is possible)
if (_canGoPrevious(state) || if (_canGoPrevious(state) ||

View file

@ -11,7 +11,6 @@ class AnswerOptions extends StatelessWidget {
required this.isAnswerSubmitted, required this.isAnswerSubmitted,
required this.isCorrect, required this.isCorrect,
this.enabled = true, this.enabled = true,
this.maxHeight,
super.key, super.key,
}); });
@ -21,7 +20,6 @@ class AnswerOptions extends StatelessWidget {
final bool isAnswerSubmitted; final bool isAnswerSubmitted;
final bool isCorrect; final bool isCorrect;
final bool enabled; final bool enabled;
final double? maxHeight;
void _onAnswerSelected(BuildContext context, String option) { void _onAnswerSelected(BuildContext context, String option) {
// Note: Sound service access would be implemented through proper DI injection // Note: Sound service access would be implemented through proper DI injection
@ -48,22 +46,11 @@ class AnswerOptions extends StatelessWidget {
hasOptionItems && hasOptionItems &&
question.optionItems.any((item) => item.image != null); question.optionItems.any((item) => item.image != null);
// Adapt aspect ratio based on maxHeight // Fixed aspect ratios for consistent layout
double childAspectRatio; final childAspectRatio = hasImages ? 1.1 : 4.0;
if (hasImages) { final spacing = 12.0;
childAspectRatio = maxHeight != null && maxHeight! < 300 ? 1.0 : 1.1;
} else {
childAspectRatio = maxHeight != null && maxHeight! < 300 ? 3.0 : 4.0;
}
// Adapt spacing based on available height return GridView.builder(
final spacing = maxHeight != null && maxHeight! < 300 ? 8.0 : 12.0;
return ConstrainedBox(
constraints: maxHeight != null
? BoxConstraints(maxHeight: maxHeight!)
: const BoxConstraints(),
child: GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@ -82,7 +69,6 @@ class AnswerOptions extends StatelessWidget {
return _buildAnswerOption(context, option); return _buildAnswerOption(context, option);
} }
}, },
),
); );
}, },
); );
@ -186,7 +172,7 @@ class AnswerOptions extends StatelessWidget {
splashColor: borderColor?.withOpacity(0.1), splashColor: borderColor?.withOpacity(0.1),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: color:
@ -268,6 +254,7 @@ class AnswerOptions extends StatelessWidget {
textColor: textColor, textColor: textColor,
isSelected: isSelected, isSelected: isSelected,
isCorrectOption: isCorrectOption, isCorrectOption: isCorrectOption,
isAnswerSubmitted: isAnswerSubmitted,
), ),
), ),
], ],
@ -288,6 +275,7 @@ class AnswerOptions extends StatelessWidget {
Color? textColor, Color? textColor,
required bool isSelected, required bool isSelected,
required bool isCorrectOption, required bool isCorrectOption,
required bool isAnswerSubmitted,
}) { }) {
// If we have an image, show it (with optional text below) // If we have an image, show it (with optional text below)
if (image != null) { if (image != null) {
@ -345,7 +333,7 @@ class AnswerOptions extends StatelessWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
style: Theme.of(context).textTheme.bodyLarge!.copyWith( style: Theme.of(context).textTheme.bodyLarge!.copyWith(
color: textColor, color: textColor,
fontWeight: isSelected || isCorrectOption fontWeight: isSelected || (isAnswerSubmitted && isCorrectOption)
? FontWeight.w600 ? FontWeight.w600
: FontWeight.normal, : FontWeight.normal,
fontSize: isSelected ? 17 : 16, fontSize: isSelected ? 17 : 16,

View file

@ -7,10 +7,9 @@ import '../../../domain/models/game_question.dart';
/// Widget for input letters questions - user types letters to fill in blanks /// Widget for input letters questions - user types letters to fill in blanks
class InputLettersWidget extends StatefulWidget { class InputLettersWidget extends StatefulWidget {
const InputLettersWidget({required this.question, this.maxHeight, super.key}); const InputLettersWidget({required this.question, super.key});
final InputLettersQuestion question; final InputLettersQuestion question;
final double? maxHeight;
@override @override
State<InputLettersWidget> createState() => _InputLettersWidgetState(); State<InputLettersWidget> createState() => _InputLettersWidgetState();
@ -72,22 +71,16 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Adapt spacing and padding based on maxHeight // Fixed spacing and padding for consistent layout
final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; final containerPadding = 16.w;
final containerPadding = isCompact ? 12.w : 16.w; final containerMargin = 16.h;
final containerMargin = isCompact ? 12.h : 16.h; final spacingAfterTitle = 12.h;
final spacingAfterTitle = isCompact ? 8.h : 12.h; final spacingAfterWord = 12.h;
final spacingAfterWord = isCompact ? 8.h : 12.h; final spacingAfterGrid = 16.h;
final spacingAfterGrid = isCompact ? 12.h : 16.h;
final showFillInstruction = !isCompact; // Hide on very compact screens
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return ConstrainedBox( return Column(
constraints: widget.maxHeight != null
? BoxConstraints(maxHeight: widget.maxHeight!)
: const BoxConstraints(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -110,15 +103,13 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (showFillInstruction)
Text( Text(
'Fill in the blanks:', 'Fill in the blanks:',
style: Theme.of(context).textTheme.titleLarge style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
if (showFillInstruction && if (widget.question.word.isNotEmpty)
widget.question.word.isNotEmpty)
SizedBox(height: spacingAfterTitle), SizedBox(height: spacingAfterTitle),
if (widget.question.word.isNotEmpty) ...[ if (widget.question.word.isNotEmpty) ...[
Text( Text(
@ -208,12 +199,11 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
final parts = <Widget>[]; final parts = <Widget>[];
int inputIndex = 0; int inputIndex = 0;
// Adapt sizes based on maxHeight // Fixed cell sizes for consistent layout
final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; final cellWidth = 28.w;
final cellWidth = isCompact ? 24.w : 28.w; final cellHeight = 48.h;
final cellHeight = isCompact ? 36.h : 48.h; final cellFontSize = 20.sp;
final cellFontSize = isCompact ? 16.sp : 20.sp; final blankCellWidth = 32.w;
final blankCellWidth = isCompact ? 28.w : 32.w;
for (int i = 0; i < template.length; i++) { for (int i = 0; i < template.length; i++) {
final char = template[i]; final char = template[i];
@ -283,11 +273,8 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
Widget _buildButtonsGrid(BoxConstraints constraints) { Widget _buildButtonsGrid(BoxConstraints constraints) {
final hasImages = widget.question.buttons.any((b) => b.image != null); final hasImages = widget.question.buttons.any((b) => b.image != null);
final crossAxisCount = constraints.maxWidth > 600 ? 4 : 3; final crossAxisCount = constraints.maxWidth > 600 ? 4 : 3;
final isCompact = widget.maxHeight != null && widget.maxHeight! < 400; final childAspectRatio = hasImages ? 1.2 : 2.0;
final childAspectRatio = hasImages final spacing = 12.0;
? (isCompact ? 1.0 : 1.2)
: (isCompact ? 2.0 : 2.0);
final spacing = isCompact ? 8.0 : 12.0;
return GridView.builder( return GridView.builder(
shrinkWrap: true, shrinkWrap: true,

View file

@ -7,10 +7,9 @@ import '../../../domain/models/game_question.dart';
/// Widget for match questions - user connects items from two columns /// Widget for match questions - user connects items from two columns
class MatchWidget extends StatefulWidget { class MatchWidget extends StatefulWidget {
const MatchWidget({required this.question, this.maxHeight, super.key}); const MatchWidget({required this.question, super.key});
final MatchQuestion question; final MatchQuestion question;
final double? maxHeight;
@override @override
State<MatchWidget> createState() => _MatchWidgetState(); State<MatchWidget> createState() => _MatchWidgetState();
@ -23,26 +22,18 @@ class _MatchWidgetState extends State<MatchWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Adapt spacing and padding based on maxHeight // Fixed spacing and padding for consistent layout
final isCompact = widget.maxHeight != null && widget.maxHeight! < 500; final instructionPadding = 16.w;
final instructionPadding = isCompact ? 12.w : 16.w; final instructionMargin = 16.h;
final instructionMargin = isCompact ? 12.h : 16.h; final connectionPadding = 16.w;
final connectionPadding = isCompact ? 12.w : 16.w; final connectionMargin = 16.h;
final connectionMargin = isCompact ? 12.h : 16.h; final spacingAfterColumns = 16.h;
final spacingAfterColumns = isCompact ? 12.h : 16.h;
final instructionText = isCompact
? 'Connect matching items'
: 'Connect the matching items by tapping them in order';
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isWideScreen = constraints.maxWidth > 600; final isWideScreen = constraints.maxWidth > 600;
return ConstrainedBox( return Column(
constraints: widget.maxHeight != null
? BoxConstraints(maxHeight: widget.maxHeight!)
: const BoxConstraints(),
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Instructions // Instructions
@ -61,7 +52,7 @@ class _MatchWidgetState extends State<MatchWidget> {
], ],
), ),
child: Text( child: Text(
instructionText, 'Connect the matching items by tapping them in order',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -87,11 +78,9 @@ class _MatchWidgetState extends State<MatchWidget> {
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
), ),
if (!isCompact) ...[
SizedBox(height: 8.h), SizedBox(height: 8.h),
..._buildConnectionDisplay(), ..._buildConnectionDisplay(),
], ],
],
), ),
), ),
], ],
@ -108,7 +97,7 @@ class _MatchWidgetState extends State<MatchWidget> {
isLeft: true, isLeft: true,
), ),
), ),
SizedBox(width: isCompact ? 16.w : 24.w), SizedBox(width: 24.w),
Expanded( Expanded(
child: _buildColumn( child: _buildColumn(
widget.question.rightItems, widget.question.rightItems,
@ -128,14 +117,14 @@ class _MatchWidgetState extends State<MatchWidget> {
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: isCompact ? 8.h : 12.h), SizedBox(height: 12.h),
Flexible( Flexible(
child: _buildColumn( child: _buildColumn(
widget.question.leftItems, widget.question.leftItems,
isLeft: true, isLeft: true,
), ),
), ),
SizedBox(height: isCompact ? 16.h : 24.h), SizedBox(height: 24.h),
Text( Text(
'Right Column', 'Right Column',
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
@ -147,7 +136,7 @@ class _MatchWidgetState extends State<MatchWidget> {
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: isCompact ? 8.h : 12.h), SizedBox(height: 12.h),
Flexible( Flexible(
child: _buildColumn( child: _buildColumn(
widget.question.rightItems, widget.question.rightItems,
@ -178,18 +167,16 @@ class _MatchWidgetState extends State<MatchWidget> {
} }
Widget _buildColumn(List<MatchItem> items, {required bool isLeft}) { Widget _buildColumn(List<MatchItem> items, {required bool isLeft}) {
final isCompact = widget.maxHeight != null && widget.maxHeight! < 500; // Fixed sizes for consistent layout
final itemMargin = isCompact ? 4.h : 6.h; final itemMargin = 6.h;
final itemPadding = isCompact ? 8.w : 12.w; final itemPadding = 12.w;
final imageSize = isCompact ? 32.0 : 40.0; final imageSize = 40.0;
final fontSize = isCompact ? 14.sp : 16.sp; final fontSize = 16.sp;
final iconSize = isCompact ? 18.sp : 20.sp; final iconSize = 20.sp;
return ListView.builder( return ListView.builder(
itemCount: items.length, itemCount: items.length,
itemExtent: isCompact itemExtent: 64.0, // Fixed item height for better performance
? 52.0
: 64.0, // Fixed item height for better performance
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = items[index]; final item = items[index];
final isSelected = isLeft final isSelected = isLeft
@ -240,7 +227,7 @@ class _MatchWidgetState extends State<MatchWidget> {
), ),
), ),
), ),
SizedBox(width: isCompact ? 8.w : 12.w), SizedBox(width: 12.w),
], ],
Expanded( Expanded(
child: Text( child: Text(

View file

@ -21,14 +21,12 @@ class MatrixWidget extends StatefulWidget {
required this.question, required this.question,
this.onWrongAttempt, this.onWrongAttempt,
this.onCompleted, this.onCompleted,
this.maxHeight,
super.key, super.key,
}); });
final MatrixQuestion question; final MatrixQuestion question;
final Future<void> Function()? onWrongAttempt; final Future<void> Function()? onWrongAttempt;
final Future<void> Function(MatrixImageSelectAnswer answer)? onCompleted; final Future<void> Function(MatrixImageSelectAnswer answer)? onCompleted;
final double? maxHeight;
@override @override
State<MatrixWidget> createState() => _MatrixWidgetState(); State<MatrixWidget> createState() => _MatrixWidgetState();
@ -175,44 +173,17 @@ class _MatrixWidgetState extends State<MatrixWidget>
...List<MatrixCard?>.filled(total - _slots.length, null), ...List<MatrixCard?>.filled(total - _slots.length, null),
]; ];
// Calculate available height for grid (reserve space for target container) // Fixed spacing values for consistent layout
// Target container: ~100-120px (compact) to 140-160px (full) final spacingBetween = 16.h;
final targetContainerHeight = final cellSpacing = 10.0;
widget.maxHeight != null && widget.maxHeight! < 350 ? 100.0 : 140.0; final targetPaddingH = 20.w;
final spacingBetween = widget.maxHeight != null && widget.maxHeight! < 350 final targetPaddingV = 16.h;
? 12.h
: 16.h;
final gridMaxHeight = widget.maxHeight != null return Column(
? 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, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible( Flexible(
child: ConstrainedBox(
constraints: gridMaxHeight != null
? BoxConstraints(maxHeight: gridMaxHeight)
: const BoxConstraints(),
child: GridView.builder( child: GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -229,7 +200,6 @@ class _MatrixWidgetState extends State<MatrixWidget>
}, },
), ),
), ),
),
SizedBox(height: spacingBetween), SizedBox(height: spacingBetween),
Container( Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(

View file

@ -10,13 +10,11 @@ class QuestionDisplay extends StatelessWidget {
const QuestionDisplay({ const QuestionDisplay({
required this.question, required this.question,
this.onPlayAudio, this.onPlayAudio,
this.maxHeight,
super.key, super.key,
}); });
final GameQuestion question; final GameQuestion question;
final QuestionAudioPlayback? onPlayAudio; final QuestionAudioPlayback? onPlayAudio;
final double? maxHeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -78,43 +76,17 @@ class QuestionDisplay extends StatelessWidget {
}) { }) {
final theme = Theme.of(context).textTheme; final theme = Theme.of(context).textTheme;
// Calculate available height for image (30-40% of maxHeight, min 120, max 250) // Fixed spacing values for consistent layout
double? imageMaxHeight; final spacingAfterImage = 12.h;
if (maxHeight != null) { final spacingAfterText = 8.h;
final imageHeight = maxHeight! * 0.35;
imageMaxHeight = imageHeight.clamp(120.0, 250.0);
} else {
imageMaxHeight = 200.h;
}
// Calculate spacing based on available height return Column(
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, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Image display // Image display
if (image != null) ...[ if (image != null) ...[
Flexible( Flexible(
child: Container(
constraints: BoxConstraints(
maxHeight: imageMaxHeight,
maxWidth: double.infinity,
),
child: Image.network( child: Image.network(
image, image,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -137,7 +109,6 @@ class QuestionDisplay extends StatelessWidget {
}, },
), ),
), ),
),
SizedBox(height: spacingAfterImage), SizedBox(height: spacingAfterImage),
], ],
@ -147,12 +118,12 @@ class QuestionDisplay extends StatelessWidget {
child: Text( child: Text(
text, text,
style: theme.headlineSmall?.copyWith( style: theme.headlineSmall?.copyWith(
fontSize: fontSize, fontSize: 20.sp,
height: 1.4, height: 1.4,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: maxLines, maxLines: 5,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),
), ),
@ -164,7 +135,6 @@ class QuestionDisplay extends StatelessWidget {
_QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio), _QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio),
], ],
], ],
),
); );
} }
} }

View file

@ -37,8 +37,18 @@
<!-- Telegram Web App SDK --> <!-- Telegram Web App SDK -->
<script src="https://telegram.org/js/telegram-web-app.js" defer></script> <script src="https://telegram.org/js/telegram-web-app.js" defer></script>
<!-- Adsgram SDK for rewarded ads - must load before foos.js --> <!-- Adsgram SDK for rewarded ads - must load before foos.js
<script src="https://sad.adsgram.ai/js/sad.min.js"></script> <script src="https://sad.adsgram.ai/js/sad.min.js"></script>
-->
<script src="https://richinfo.co/richpartners/telegram/js/tg-ob.js"></script>
<script>
window.TelegramAdsController = new TelegramAdsController();
window.TelegramAdsController.initialize({
pubId: "996812",
appId: "5105",
});
</script>
<!-- JavaScript bridge for Adsgram SDK integration --> <!-- JavaScript bridge for Adsgram SDK integration -->
<script src="foos.js"></script> <script src="foos.js"></script>