stuff
Some checks failed
Web App CI / test (push) Has been cancelled
Deploy Mnemo Cards / Deploy Backend (push) Has been cancelled
Web App CI / build (push) Has been cancelled
Deploy Mnemo Cards / Deploy Web App (push) Has been cancelled
Deploy Mnemo Cards / Final Verification (push) Has been cancelled

This commit is contained in:
Dmitry 2025-12-21 00:03:50 +03:00
parent e7aab1d4ba
commit 9f52aa1e1e
9 changed files with 183 additions and 183 deletions

View file

@ -63,6 +63,7 @@ abstract class InputLettersQuestion with _$InputLettersQuestion {
required String template, // e.g., "H _ _ L _"
String? image,
String? audio,
String? text, // Question text to display
required String correctAnswer,
required String word,
@Default('inputLetters') String type,

View file

@ -1131,7 +1131,8 @@ as String,
mixin _$InputLettersQuestion {
String get id; String get template;// e.g., "H _ _ L _"
String? get image; String? get audio; String get correctAnswer; String get word; String get type; List<ChoiceOption> get buttons;
String? get image; String? get audio; String? get text;// Question text to display
String get correctAnswer; String get word; String get type; List<ChoiceOption> get buttons;
/// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -1144,16 +1145,16 @@ $InputLettersQuestionCopyWith<InputLettersQuestion> get copyWith => _$InputLette
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is InputLettersQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.template, template) || other.template == template)&&(identical(other.image, image) || other.image == image)&&(identical(other.audio, audio) || other.audio == audio)&&(identical(other.correctAnswer, correctAnswer) || other.correctAnswer == correctAnswer)&&(identical(other.word, word) || other.word == word)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.buttons, buttons));
return identical(this, other) || (other.runtimeType == runtimeType&&other is InputLettersQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.template, template) || other.template == template)&&(identical(other.image, image) || other.image == image)&&(identical(other.audio, audio) || other.audio == audio)&&(identical(other.text, text) || other.text == text)&&(identical(other.correctAnswer, correctAnswer) || other.correctAnswer == correctAnswer)&&(identical(other.word, word) || other.word == word)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.buttons, buttons));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,template,image,audio,correctAnswer,word,type,const DeepCollectionEquality().hash(buttons));
int get hashCode => Object.hash(runtimeType,id,template,image,audio,text,correctAnswer,word,type,const DeepCollectionEquality().hash(buttons));
@override
String toString() {
return 'InputLettersQuestion(id: $id, template: $template, image: $image, audio: $audio, correctAnswer: $correctAnswer, word: $word, type: $type, buttons: $buttons)';
return 'InputLettersQuestion(id: $id, template: $template, image: $image, audio: $audio, text: $text, correctAnswer: $correctAnswer, word: $word, type: $type, buttons: $buttons)';
}
@ -1164,7 +1165,7 @@ abstract mixin class $InputLettersQuestionCopyWith<$Res> {
factory $InputLettersQuestionCopyWith(InputLettersQuestion value, $Res Function(InputLettersQuestion) _then) = _$InputLettersQuestionCopyWithImpl;
@useResult
$Res call({
String id, String template, String? image, String? audio, String correctAnswer, String word, String type, List<ChoiceOption> buttons
String id, String template, String? image, String? audio, String? text, String correctAnswer, String word, String type, List<ChoiceOption> buttons
});
@ -1181,12 +1182,13 @@ class _$InputLettersQuestionCopyWithImpl<$Res>
/// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? template = null,Object? image = freezed,Object? audio = freezed,Object? correctAnswer = null,Object? word = null,Object? type = null,Object? buttons = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? template = null,Object? image = freezed,Object? audio = freezed,Object? text = freezed,Object? correctAnswer = null,Object? word = null,Object? type = null,Object? buttons = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,template: null == template ? _self.template : template // ignore: cast_nullable_to_non_nullable
as String,image: freezed == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as String?,audio: freezed == audio ? _self.audio : audio // ignore: cast_nullable_to_non_nullable
as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String?,correctAnswer: null == correctAnswer ? _self.correctAnswer : correctAnswer // ignore: cast_nullable_to_non_nullable
as String,word: null == word ? _self.word : word // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@ -1276,10 +1278,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String template, String? image, String? audio, String correctAnswer, String word, String type, List<ChoiceOption> buttons)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String template, String? image, String? audio, String? text, String correctAnswer, String word, String type, List<ChoiceOption> buttons)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _InputLettersQuestion() when $default != null:
return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
return $default(_that.id,_that.template,_that.image,_that.audio,_that.text,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
return orElse();
}
@ -1297,10 +1299,10 @@ return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAns
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String template, String? image, String? audio, String correctAnswer, String word, String type, List<ChoiceOption> buttons) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String template, String? image, String? audio, String? text, String correctAnswer, String word, String type, List<ChoiceOption> buttons) $default,) {final _that = this;
switch (_that) {
case _InputLettersQuestion():
return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
return $default(_that.id,_that.template,_that.image,_that.audio,_that.text,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
throw StateError('Unexpected subclass');
}
@ -1317,10 +1319,10 @@ return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAns
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String template, String? image, String? audio, String correctAnswer, String word, String type, List<ChoiceOption> buttons)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String template, String? image, String? audio, String? text, String correctAnswer, String word, String type, List<ChoiceOption> buttons)? $default,) {final _that = this;
switch (_that) {
case _InputLettersQuestion() when $default != null:
return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
return $default(_that.id,_that.template,_that.image,_that.audio,_that.text,_that.correctAnswer,_that.word,_that.type,_that.buttons);case _:
return null;
}
@ -1332,7 +1334,7 @@ return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAns
@JsonSerializable()
class _InputLettersQuestion implements InputLettersQuestion {
const _InputLettersQuestion({required this.id, required this.template, this.image, this.audio, required this.correctAnswer, required this.word, this.type = 'inputLetters', final List<ChoiceOption> buttons = const []}): _buttons = buttons;
const _InputLettersQuestion({required this.id, required this.template, this.image, this.audio, this.text, required this.correctAnswer, required this.word, this.type = 'inputLetters', final List<ChoiceOption> buttons = const []}): _buttons = buttons;
factory _InputLettersQuestion.fromJson(Map<String, dynamic> json) => _$InputLettersQuestionFromJson(json);
@override final String id;
@ -1340,6 +1342,8 @@ class _InputLettersQuestion implements InputLettersQuestion {
// e.g., "H _ _ L _"
@override final String? image;
@override final String? audio;
@override final String? text;
// Question text to display
@override final String correctAnswer;
@override final String word;
@override@JsonKey() final String type;
@ -1364,16 +1368,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _InputLettersQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.template, template) || other.template == template)&&(identical(other.image, image) || other.image == image)&&(identical(other.audio, audio) || other.audio == audio)&&(identical(other.correctAnswer, correctAnswer) || other.correctAnswer == correctAnswer)&&(identical(other.word, word) || other.word == word)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._buttons, _buttons));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _InputLettersQuestion&&(identical(other.id, id) || other.id == id)&&(identical(other.template, template) || other.template == template)&&(identical(other.image, image) || other.image == image)&&(identical(other.audio, audio) || other.audio == audio)&&(identical(other.text, text) || other.text == text)&&(identical(other.correctAnswer, correctAnswer) || other.correctAnswer == correctAnswer)&&(identical(other.word, word) || other.word == word)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._buttons, _buttons));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,template,image,audio,correctAnswer,word,type,const DeepCollectionEquality().hash(_buttons));
int get hashCode => Object.hash(runtimeType,id,template,image,audio,text,correctAnswer,word,type,const DeepCollectionEquality().hash(_buttons));
@override
String toString() {
return 'InputLettersQuestion(id: $id, template: $template, image: $image, audio: $audio, correctAnswer: $correctAnswer, word: $word, type: $type, buttons: $buttons)';
return 'InputLettersQuestion(id: $id, template: $template, image: $image, audio: $audio, text: $text, correctAnswer: $correctAnswer, word: $word, type: $type, buttons: $buttons)';
}
@ -1384,7 +1388,7 @@ abstract mixin class _$InputLettersQuestionCopyWith<$Res> implements $InputLette
factory _$InputLettersQuestionCopyWith(_InputLettersQuestion value, $Res Function(_InputLettersQuestion) _then) = __$InputLettersQuestionCopyWithImpl;
@override @useResult
$Res call({
String id, String template, String? image, String? audio, String correctAnswer, String word, String type, List<ChoiceOption> buttons
String id, String template, String? image, String? audio, String? text, String correctAnswer, String word, String type, List<ChoiceOption> buttons
});
@ -1401,12 +1405,13 @@ class __$InputLettersQuestionCopyWithImpl<$Res>
/// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? template = null,Object? image = freezed,Object? audio = freezed,Object? correctAnswer = null,Object? word = null,Object? type = null,Object? buttons = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? template = null,Object? image = freezed,Object? audio = freezed,Object? text = freezed,Object? correctAnswer = null,Object? word = null,Object? type = null,Object? buttons = null,}) {
return _then(_InputLettersQuestion(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,template: null == template ? _self.template : template // ignore: cast_nullable_to_non_nullable
as String,image: freezed == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as String?,audio: freezed == audio ? _self.audio : audio // ignore: cast_nullable_to_non_nullable
as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String?,correctAnswer: null == correctAnswer ? _self.correctAnswer : correctAnswer // ignore: cast_nullable_to_non_nullable
as String,word: null == word ? _self.word : word // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable

View file

@ -113,6 +113,7 @@ _InputLettersQuestion _$InputLettersQuestionFromJson(
template: json['template'] as String,
image: json['image'] as String?,
audio: json['audio'] as String?,
text: json['text'] as String?,
correctAnswer: json['correctAnswer'] as String,
word: json['word'] as String,
type: json['type'] as String? ?? 'inputLetters',
@ -130,6 +131,7 @@ Map<String, dynamic> _$InputLettersQuestionToJson(
'template': instance.template,
'image': instance.image,
'audio': instance.audio,
'text': instance.text,
'correctAnswer': instance.correctAnswer,
'word': instance.word,
'type': instance.type,

View file

@ -511,6 +511,7 @@ class TestsStateManager extends StateManager<TestsState> {
question.imageUrl ??
question.image, // Use presigned URL if available
audio: question.audio,
text: question.text, // Question text to display
correctAnswer: question.answer,
word: question.word,
buttons: optionItems,

View file

@ -396,38 +396,36 @@ class _GamePageState extends State<GamePage> {
minHeight: 200.h,
),
child: currentQuestion.when(
multipleChoice: (q) => SingleChildScrollView(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: Container(
key: ValueKey(
'question_input_${currentQuestion.hashCode}',
),
child: AnswerOptions(
question: q,
selectedAnswer:
_getSelectedAnswerForMultipleChoice(
q,
questionResults,
),
onAnswerSelected: _onAnswerSelected,
isAnswerSubmitted: isAnswerSubmitted,
isCorrect: isCorrect,
multipleChoice: (q) => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: Container(
key: ValueKey(
'question_input_${currentQuestion.hashCode}',
),
child: AnswerOptions(
question: q,
selectedAnswer:
_getSelectedAnswerForMultipleChoice(
q,
questionResults,
),
onAnswerSelected: _onAnswerSelected,
isAnswerSubmitted: isAnswerSubmitted,
isCorrect: isCorrect,
),
),
),

View file

@ -61,18 +61,19 @@ class AnswerOptions extends StatelessWidget {
// Aspect ratio = width / height
final itemWidth = constraints.maxWidth / crossAxisCount - spacing;
childAspectRatio = itemWidth / itemHeight;
// Clamp to reasonable values
// For images, ensure we have enough vertical space
// Clamp to reasonable values - images need more vertical space (lower aspect ratio)
childAspectRatio = childAspectRatio.clamp(
hasImages ? 0.8 : 2.0,
hasImages ? 1.5 : 6.0,
hasImages ? 0.7 : 2.0,
hasImages ? 1.2 : 6.0,
);
} else {
// Fallback to fixed aspect ratios
childAspectRatio = hasImages ? 1.1 : 4.0;
// For images, use lower aspect ratio to give more vertical space
childAspectRatio = hasImages ? 0.9 : 4.0;
}
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,

View file

@ -71,73 +71,73 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
@override
Widget build(BuildContext context) {
// 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;
// Fixed spacing for consistent layout
final spacingAfterImage = 16.h;
final spacingAfterTemplate = 16.h;
return LayoutBuilder(
builder: (context, constraints) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Display the template with current input
// Image and template display
Expanded(
flex: 2,
child: SingleChildScrollView(
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,
flex: widget.question.buttons.isNotEmpty ? 2 : 4,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Image display (if available)
if (widget.question.image != null) ...[
Expanded(
flex: 3,
child: Center(
child: Image.network(
widget.question.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),
),
textAlign: TextAlign.center,
child: Icon(
Icons.image_not_supported,
size: 48.sp,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
);
},
),
],
SizedBox(height: spacingAfterWord),
_buildTemplateDisplay(),
],
),
),
SizedBox(height: spacingAfterImage),
],
// Template display (only blanks, no letters)
Expanded(
flex: widget.question.image != null ? 2 : 3,
child: Center(
child: _buildTemplateDisplay(),
),
),
),
],
),
),
SizedBox(height: spacingAfterTemplate),
// Buttons with images or text (if available)
if (widget.question.buttons.isNotEmpty) ...[
Expanded(
flex: 3,
flex: 5,
child: _buildButtonsGrid(constraints),
),
SizedBox(height: spacingAfterGrid),
] else ...[
// Input field (only if no buttons)
Container(
@ -169,9 +169,10 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
onSubmitted: _submitAnswer,
),
),
SizedBox(height: spacingAfterGrid),
],
SizedBox(height: 16.h),
// Submit button
ElevatedButton.icon(
onPressed: _currentAnswer.isNotEmpty ? _submitAnswer : null,
@ -204,11 +205,12 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
int inputIndex = 0;
// Fixed cell sizes for consistent layout
final cellWidth = 28.w;
final cellHeight = 48.h;
final cellFontSize = 20.sp;
final blankCellWidth = 32.w;
final spacingBetweenBlanks = 8.w;
// Only show blanks (squares for input), ignore other characters
for (int i = 0; i < template.length; i++) {
final char = template[i];
@ -223,7 +225,7 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
Container(
width: blankCellWidth,
height: cellHeight,
margin: EdgeInsets.symmetric(horizontal: 2.w),
margin: EdgeInsets.symmetric(horizontal: spacingBetweenBlanks / 2),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
@ -246,29 +248,8 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
),
),
);
} else if (char == ' ') {
// Space character - add small spacing
parts.add(SizedBox(width: 8.w));
} else {
// This is a regular character
parts.add(
Container(
width: cellWidth,
height: cellHeight,
margin: EdgeInsets.symmetric(horizontal: 2.w),
alignment: Alignment.center,
child: Text(
char,
style: TextStyle(
fontSize: cellFontSize,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
),
);
}
// Ignore other characters (letters and spaces) - don't display them
}
return parts;

View file

@ -179,63 +179,72 @@ class _MatrixWidgetState extends State<MatrixWidget>
final targetPaddingH = 20.w;
final targetPaddingV = 16.h;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: GridView.builder(
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(
horizontal: targetPaddingH,
vertical: targetPaddingV,
),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: colorScheme.primary, width: 3),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
return LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
height: constraints.maxHeight.isFinite && constraints.maxHeight > 0
? constraints.maxHeight
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: GridView.builder(
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(
horizontal: targetPaddingH,
vertical: targetPaddingV,
),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(color: colorScheme.primary, width: 3),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.question.text != null &&
widget.question.text!.isNotEmpty)
Text(
widget.question.text!,
style: textTheme.labelLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
if (widget.question.text != null &&
widget.question.text!.isNotEmpty)
SizedBox(height: 8.h),
_buildTargetContent(context),
],
),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.question.text != null &&
widget.question.text!.isNotEmpty)
Text(
widget.question.text!,
style: textTheme.labelLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
if (widget.question.text != null &&
widget.question.text!.isNotEmpty)
SizedBox(height: 8.h),
_buildTargetContent(context),
],
),
),
],
);
},
);
}

View file

@ -42,9 +42,11 @@ class QuestionDisplay extends StatelessWidget {
InputLettersQuestion question,
BuildContext context,
) {
// For input letters questions, show text (if available), image and audio
// Template is displayed in InputLettersWidget itself
return _buildQuestionContent(
context: context,
text: question.template, // Display the template with blanks
text: question.text ?? '', // Display question text if available
image: question.image,
audio: question.audio,
);