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 _" required String template, // e.g., "H _ _ L _"
String? image, String? image,
String? audio, String? audio,
String? text, // Question text to display
required String correctAnswer, required String correctAnswer,
required String word, required String word,
@Default('inputLetters') String type, @Default('inputLetters') String type,

View file

@ -1131,7 +1131,8 @@ as String,
mixin _$InputLettersQuestion { mixin _$InputLettersQuestion {
String get id; String get template;// e.g., "H _ _ L _" 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 /// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -1144,16 +1145,16 @@ $InputLettersQuestionCopyWith<InputLettersQuestion> get copyWith => _$InputLette
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $InputLettersQuestionCopyWith(InputLettersQuestion value, $Res Function(InputLettersQuestion) _then) = _$InputLettersQuestionCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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,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?,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?,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,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 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) { switch (_that) {
case _InputLettersQuestion() when $default != null: 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(); 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) { switch (_that) {
case _InputLettersQuestion(): 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'); 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) { switch (_that) {
case _InputLettersQuestion() when $default != null: 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; return null;
} }
@ -1332,7 +1334,7 @@ return $default(_that.id,_that.template,_that.image,_that.audio,_that.correctAns
@JsonSerializable() @JsonSerializable()
class _InputLettersQuestion implements InputLettersQuestion { 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); factory _InputLettersQuestion.fromJson(Map<String, dynamic> json) => _$InputLettersQuestionFromJson(json);
@override final String id; @override final String id;
@ -1340,6 +1342,8 @@ class _InputLettersQuestion implements InputLettersQuestion {
// e.g., "H _ _ L _" // e.g., "H _ _ L _"
@override final String? image; @override final String? image;
@override final String? audio; @override final String? audio;
@override final String? text;
// Question text to display
@override final String correctAnswer; @override final String correctAnswer;
@override final String word; @override final String word;
@override@JsonKey() final String type; @override@JsonKey() final String type;
@ -1364,16 +1368,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$InputLettersQuestionCopyWith(_InputLettersQuestion value, $Res Function(_InputLettersQuestion) _then) = __$InputLettersQuestionCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of InputLettersQuestion
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_InputLettersQuestion(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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,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?,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?,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,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 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, template: json['template'] as String,
image: json['image'] as String?, image: json['image'] as String?,
audio: json['audio'] as String?, audio: json['audio'] as String?,
text: json['text'] as String?,
correctAnswer: json['correctAnswer'] as String, correctAnswer: json['correctAnswer'] as String,
word: json['word'] as String, word: json['word'] as String,
type: json['type'] as String? ?? 'inputLetters', type: json['type'] as String? ?? 'inputLetters',
@ -130,6 +131,7 @@ Map<String, dynamic> _$InputLettersQuestionToJson(
'template': instance.template, 'template': instance.template,
'image': instance.image, 'image': instance.image,
'audio': instance.audio, 'audio': instance.audio,
'text': instance.text,
'correctAnswer': instance.correctAnswer, 'correctAnswer': instance.correctAnswer,
'word': instance.word, 'word': instance.word,
'type': instance.type, 'type': instance.type,

View file

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

View file

@ -396,38 +396,36 @@ class _GamePageState extends State<GamePage> {
minHeight: 200.h, minHeight: 200.h,
), ),
child: currentQuestion.when( child: currentQuestion.when(
multipleChoice: (q) => SingleChildScrollView( multipleChoice: (q) => AnimatedSwitcher(
child: AnimatedSwitcher( duration: const Duration(milliseconds: 400),
duration: const Duration(milliseconds: 400), switchInCurve: Curves.easeInOut,
switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut, transitionBuilder: (child, animation) {
transitionBuilder: (child, animation) { return FadeTransition(
return FadeTransition( opacity: animation,
opacity: animation, child: SlideTransition(
child: SlideTransition( position: Tween<Offset>(
position: Tween<Offset>( begin: const Offset(0.05, 0),
begin: const Offset(0.05, 0), end: Offset.zero,
end: Offset.zero, ).animate(animation),
).animate(animation), child: child,
child: child,
),
);
},
child: Container(
key: ValueKey(
'question_input_${currentQuestion.hashCode}',
),
child: AnswerOptions(
question: q,
selectedAnswer:
_getSelectedAnswerForMultipleChoice(
q,
questionResults,
),
onAnswerSelected: _onAnswerSelected,
isAnswerSubmitted: isAnswerSubmitted,
isCorrect: isCorrect,
), ),
);
},
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 // Aspect ratio = width / height
final itemWidth = constraints.maxWidth / crossAxisCount - spacing; final itemWidth = constraints.maxWidth / crossAxisCount - spacing;
childAspectRatio = itemWidth / itemHeight; 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( childAspectRatio = childAspectRatio.clamp(
hasImages ? 0.8 : 2.0, hasImages ? 0.7 : 2.0,
hasImages ? 1.5 : 6.0, hasImages ? 1.2 : 6.0,
); );
} else { } else {
// Fallback to fixed aspect ratios // 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( return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,

View file

@ -71,73 +71,73 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Fixed spacing and padding for consistent layout // Fixed spacing for consistent layout
final containerPadding = 16.w; final spacingAfterImage = 16.h;
final containerMargin = 16.h; final spacingAfterTemplate = 16.h;
final spacingAfterTitle = 12.h;
final spacingAfterWord = 12.h;
final spacingAfterGrid = 16.h;
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Display the template with current input // Image and template display
Expanded( Expanded(
flex: 2, flex: widget.question.buttons.isNotEmpty ? 2 : 4,
child: SingleChildScrollView( child: Column(
child: Container( mainAxisAlignment: MainAxisAlignment.center,
padding: EdgeInsets.all(containerPadding), children: [
margin: EdgeInsets.only(bottom: containerMargin), // Image display (if available)
decoration: BoxDecoration( if (widget.question.image != null) ...[
color: Theme.of(context).colorScheme.surface, Expanded(
borderRadius: BorderRadius.circular(16.r), flex: 3,
boxShadow: [ child: Center(
BoxShadow( child: Image.network(
color: Colors.black.withOpacity(0.1), widget.question.image!,
blurRadius: 8, fit: BoxFit.contain,
offset: const Offset(0, 2), errorBuilder: (context, error, stackTrace) {
), return Container(
], height: 120.h,
), width: 120.w,
child: Column( decoration: BoxDecoration(
mainAxisSize: MainAxisSize.min, color: Theme.of(
children: [ context,
Text( ).colorScheme.surfaceContainerHighest,
'Fill in the blanks:', borderRadius: BorderRadius.circular(8.r),
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: 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) // Buttons with images or text (if available)
if (widget.question.buttons.isNotEmpty) ...[ if (widget.question.buttons.isNotEmpty) ...[
Expanded( Expanded(
flex: 3, flex: 5,
child: _buildButtonsGrid(constraints), child: _buildButtonsGrid(constraints),
), ),
SizedBox(height: spacingAfterGrid),
] else ...[ ] else ...[
// Input field (only if no buttons) // Input field (only if no buttons)
Container( Container(
@ -169,9 +169,10 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
onSubmitted: _submitAnswer, onSubmitted: _submitAnswer,
), ),
), ),
SizedBox(height: spacingAfterGrid),
], ],
SizedBox(height: 16.h),
// Submit button // Submit button
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _currentAnswer.isNotEmpty ? _submitAnswer : null, onPressed: _currentAnswer.isNotEmpty ? _submitAnswer : null,
@ -204,11 +205,12 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
int inputIndex = 0; int inputIndex = 0;
// Fixed cell sizes for consistent layout // Fixed cell sizes for consistent layout
final cellWidth = 28.w;
final cellHeight = 48.h; final cellHeight = 48.h;
final cellFontSize = 20.sp; final cellFontSize = 20.sp;
final blankCellWidth = 32.w; 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++) { for (int i = 0; i < template.length; i++) {
final char = template[i]; final char = template[i];
@ -223,7 +225,7 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
Container( Container(
width: blankCellWidth, width: blankCellWidth,
height: cellHeight, height: cellHeight,
margin: EdgeInsets.symmetric(horizontal: 2.w), margin: EdgeInsets.symmetric(horizontal: spacingBetweenBlanks / 2),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.primary, 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; return parts;

View file

@ -179,63 +179,72 @@ class _MatrixWidgetState extends State<MatrixWidget>
final targetPaddingH = 20.w; final targetPaddingH = 20.w;
final targetPaddingV = 16.h; final targetPaddingV = 16.h;
return Column( return LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.stretch, builder: (context, constraints) {
children: [ return SizedBox(
Expanded( height: constraints.maxHeight.isFinite && constraints.maxHeight > 0
child: GridView.builder( ? constraints.maxHeight
physics: const NeverScrollableScrollPhysics(), : null,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( child: Column(
crossAxisCount: size, crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisSpacing: cellSpacing, children: [
mainAxisSpacing: cellSpacing, Expanded(
childAspectRatio: 1.0, child: GridView.builder(
), physics: const NeverScrollableScrollPhysics(),
itemCount: total, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
itemBuilder: (context, index) { crossAxisCount: size,
final card = slots[index]; crossAxisSpacing: cellSpacing,
return _buildSlot(context, card); mainAxisSpacing: cellSpacing,
}, childAspectRatio: 1.0,
), ),
), itemCount: total,
SizedBox(height: spacingBetween), itemBuilder: (context, index) {
Container( final card = slots[index];
padding: EdgeInsets.symmetric( return _buildSlot(context, card);
horizontal: targetPaddingH, },
vertical: targetPaddingV, ),
), ),
decoration: BoxDecoration( SizedBox(height: spacingBetween),
color: colorScheme.surface, Container(
borderRadius: BorderRadius.circular(24.r), padding: EdgeInsets.symmetric(
border: Border.all(color: colorScheme.primary, width: 3), horizontal: targetPaddingH,
boxShadow: [ vertical: targetPaddingV,
BoxShadow( ),
color: colorScheme.shadow.withOpacity(0.3), decoration: BoxDecoration(
blurRadius: 20, color: colorScheme.surface,
offset: const Offset(0, 10), 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, InputLettersQuestion question,
BuildContext context, BuildContext context,
) { ) {
// For input letters questions, show text (if available), image and audio
// Template is displayed in InputLettersWidget itself
return _buildQuestionContent( return _buildQuestionContent(
context: context, context: context,
text: question.template, // Display the template with blanks text: question.text ?? '', // Display question text if available
image: question.image, image: question.image,
audio: question.audio, audio: question.audio,
); );