stuff
Some checks are pending
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
Some checks are pending
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:
parent
9157706cad
commit
82bc441b57
7 changed files with 391 additions and 332 deletions
|
|
@ -309,43 +309,50 @@ class _GamePageState extends State<GamePage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Question display
|
// Question display
|
||||||
Flexible(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: AnimatedSwitcher(
|
child: ConstrainedBox(
|
||||||
duration: const Duration(milliseconds: 300),
|
constraints: BoxConstraints(
|
||||||
child: KeyedSubtree(
|
minHeight: 150.h,
|
||||||
key: questionKey,
|
),
|
||||||
child: Material(
|
child: SingleChildScrollView(
|
||||||
key: GamePage.questionCardKey,
|
child: AnimatedSwitcher(
|
||||||
color: colorScheme.surface,
|
duration: const Duration(milliseconds: 300),
|
||||||
surfaceTintColor: colorScheme.surfaceTint,
|
child: KeyedSubtree(
|
||||||
elevation: 3,
|
key: questionKey,
|
||||||
shadowColor: theme.shadowColor.withOpacity(
|
child: Material(
|
||||||
theme.brightness == Brightness.dark
|
key: GamePage.questionCardKey,
|
||||||
? 0.35
|
color: colorScheme.surface,
|
||||||
: 0.14,
|
surfaceTintColor: colorScheme.surfaceTint,
|
||||||
),
|
elevation: 3,
|
||||||
shape: RoundedRectangleBorder(
|
shadowColor: theme.shadowColor.withOpacity(
|
||||||
borderRadius: BorderRadius.circular(18.r),
|
theme.brightness == Brightness.dark
|
||||||
side: BorderSide(
|
? 0.35
|
||||||
color: colorScheme.outlineVariant,
|
: 0.14,
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
: QuestionDisplay(
|
||||||
|
question: currentQuestion,
|
||||||
|
onPlayAudio:
|
||||||
|
widget.questionAudioPlayback ??
|
||||||
|
_playQuestionAudio,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -354,47 +361,54 @@ class _GamePageState extends State<GamePage> {
|
||||||
SizedBox(height: spacingAfterQuestion),
|
SizedBox(height: spacingAfterQuestion),
|
||||||
|
|
||||||
// Answer input based on question type with smooth transitions
|
// Answer input based on question type with smooth transitions
|
||||||
Flexible(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: AnimatedSwitcher(
|
child: ConstrainedBox(
|
||||||
duration: const Duration(milliseconds: 400),
|
constraints: BoxConstraints(
|
||||||
switchInCurve: Curves.easeInOut,
|
minHeight: 200.h,
|
||||||
switchOutCurve: Curves.easeInOut,
|
),
|
||||||
transitionBuilder: (child, animation) {
|
child: SingleChildScrollView(
|
||||||
return FadeTransition(
|
child: AnimatedSwitcher(
|
||||||
opacity: animation,
|
duration: const Duration(milliseconds: 400),
|
||||||
child: SlideTransition(
|
switchInCurve: Curves.easeInOut,
|
||||||
position: Tween<Offset>(
|
switchOutCurve: Curves.easeInOut,
|
||||||
begin: const Offset(0.05, 0),
|
transitionBuilder: (child, animation) {
|
||||||
end: Offset.zero,
|
return FadeTransition(
|
||||||
).animate(animation),
|
opacity: animation,
|
||||||
child: child,
|
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: 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(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -46,30 +46,51 @@ class AnswerOptions extends StatelessWidget {
|
||||||
hasOptionItems &&
|
hasOptionItems &&
|
||||||
question.optionItems.any((item) => item.image != null);
|
question.optionItems.any((item) => item.image != null);
|
||||||
|
|
||||||
// Fixed aspect ratios for consistent layout
|
|
||||||
final childAspectRatio = hasImages ? 1.1 : 4.0;
|
|
||||||
final spacing = 12.0;
|
final spacing = 12.0;
|
||||||
|
|
||||||
return GridView.builder(
|
// Calculate childAspectRatio based on available height to fill space
|
||||||
shrinkWrap: true,
|
// If we have available height, use it to calculate aspect ratio
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
double childAspectRatio;
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
if (constraints.maxHeight.isFinite && constraints.maxHeight > 0) {
|
||||||
crossAxisCount: crossAxisCount,
|
// Calculate how many rows we need
|
||||||
crossAxisSpacing: spacing,
|
final rows = (itemCount / crossAxisCount).ceil();
|
||||||
mainAxisSpacing: spacing,
|
// Calculate height per item (accounting for spacing)
|
||||||
childAspectRatio: childAspectRatio,
|
final totalSpacing = (rows - 1) * spacing;
|
||||||
),
|
final availableHeightForItems = constraints.maxHeight - totalSpacing;
|
||||||
itemCount: itemCount,
|
final itemHeight = availableHeightForItems / rows;
|
||||||
itemBuilder: (context, index) {
|
// Aspect ratio = width / height
|
||||||
if (hasOptionItems) {
|
final itemWidth = constraints.maxWidth / crossAxisCount - spacing;
|
||||||
final optionItem = question.optionItems[index];
|
childAspectRatio = itemWidth / itemHeight;
|
||||||
return _buildAnswerOptionFromItem(context, optionItem);
|
// Clamp to reasonable values
|
||||||
} else {
|
childAspectRatio = childAspectRatio.clamp(
|
||||||
final option = question.options[index];
|
hasImages ? 0.8 : 2.0,
|
||||||
return _buildAnswerOption(context, option);
|
hasImages ? 1.5 : 6.0,
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback to fixed aspect ratios
|
||||||
|
childAspectRatio = hasImages ? 1.1 : 4.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: spacing,
|
||||||
|
mainAxisSpacing: spacing,
|
||||||
|
childAspectRatio: childAspectRatio,
|
||||||
|
),
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (hasOptionItems) {
|
||||||
|
final optionItem = question.optionItems[index];
|
||||||
|
return _buildAnswerOptionFromItem(context, optionItem);
|
||||||
|
} else {
|
||||||
|
final option = question.options[index];
|
||||||
|
return _buildAnswerOption(context, option);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,90 +82,95 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
// Display the template with current input
|
// Display the template with current input
|
||||||
Flexible(
|
Expanded(
|
||||||
child: Container(
|
flex: 2,
|
||||||
padding: EdgeInsets.all(containerPadding),
|
child: SingleChildScrollView(
|
||||||
margin: EdgeInsets.only(bottom: containerMargin),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
padding: EdgeInsets.all(containerPadding),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
margin: EdgeInsets.only(bottom: containerMargin),
|
||||||
borderRadius: BorderRadius.circular(16.r),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
color: Theme.of(context).colorScheme.surface,
|
||||||
BoxShadow(
|
borderRadius: BorderRadius.circular(16.r),
|
||||||
color: Colors.black.withOpacity(0.1),
|
boxShadow: [
|
||||||
blurRadius: 8,
|
BoxShadow(
|
||||||
offset: const Offset(0, 2),
|
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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
SizedBox(height: spacingAfterWord),
|
),
|
||||||
_buildTemplateDisplay(),
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: spacingAfterWord),
|
||||||
|
_buildTemplateDisplay(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Buttons with images or text (if available)
|
// Buttons with images or text (if available)
|
||||||
if (widget.question.buttons.isNotEmpty) ...[
|
if (widget.question.buttons.isNotEmpty) ...[
|
||||||
Flexible(child: _buildButtonsGrid(constraints)),
|
Expanded(
|
||||||
SizedBox(height: spacingAfterGrid),
|
flex: 3,
|
||||||
] else ...[
|
child: _buildButtonsGrid(constraints),
|
||||||
// Input field (only if no buttons)
|
),
|
||||||
Container(
|
SizedBox(height: spacingAfterGrid),
|
||||||
constraints: BoxConstraints(
|
] else ...[
|
||||||
maxWidth: constraints.maxWidth * 0.8,
|
// Input field (only if no buttons)
|
||||||
),
|
Container(
|
||||||
child: TextField(
|
constraints: BoxConstraints(
|
||||||
controller: _controller,
|
maxWidth: constraints.maxWidth * 0.8,
|
||||||
focusNode: _focusNode,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Type the missing letters...',
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12.r),
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Theme.of(context).colorScheme.surface,
|
|
||||||
contentPadding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.w,
|
|
||||||
vertical: 12.h,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18.sp,
|
|
||||||
letterSpacing: 2,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
maxLength: widget.question.correctAnswer.length,
|
|
||||||
onSubmitted: _submitAnswer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: spacingAfterGrid),
|
child: TextField(
|
||||||
],
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Type the missing letters...',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.r),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Theme.of(context).colorScheme.surface,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.w,
|
||||||
|
vertical: 12.h,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.sp,
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLength: widget.question.correctAnswer.length,
|
||||||
|
onSubmitted: _submitAnswer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: spacingAfterGrid),
|
||||||
|
],
|
||||||
|
|
||||||
// Submit button
|
// Submit button
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
|
@ -276,7 +281,6 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
final spacing = 12.0;
|
final spacing = 12.0;
|
||||||
|
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class _MatchWidgetState extends State<MatchWidget> {
|
||||||
final isWideScreen = constraints.maxWidth > 600;
|
final isWideScreen = constraints.maxWidth > 600;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
// Instructions
|
// Instructions
|
||||||
Container(
|
Container(
|
||||||
|
|
@ -86,84 +85,82 @@ class _MatchWidgetState extends State<MatchWidget> {
|
||||||
],
|
],
|
||||||
|
|
||||||
// Two columns layout
|
// Two columns layout
|
||||||
Flexible(
|
Expanded(
|
||||||
child: isWideScreen
|
child: SingleChildScrollView(
|
||||||
? Row(
|
child: isWideScreen
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Expanded(
|
children: [
|
||||||
child: _buildColumn(
|
Expanded(
|
||||||
|
child: _buildColumn(
|
||||||
|
widget.question.leftItems,
|
||||||
|
isLeft: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 24.w),
|
||||||
|
Expanded(
|
||||||
|
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: 12.h),
|
||||||
|
_buildColumn(
|
||||||
widget.question.leftItems,
|
widget.question.leftItems,
|
||||||
isLeft: true,
|
isLeft: true,
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 24.h),
|
||||||
SizedBox(width: 24.w),
|
Text(
|
||||||
Expanded(
|
'Right Column',
|
||||||
child: _buildColumn(
|
style: Theme.of(context).textTheme.titleMedium
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
_buildColumn(
|
||||||
widget.question.rightItems,
|
widget.question.rightItems,
|
||||||
isLeft: false,
|
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: 12.h),
|
|
||||||
Flexible(
|
|
||||||
child: _buildColumn(
|
|
||||||
widget.question.leftItems,
|
|
||||||
isLeft: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: spacingAfterColumns),
|
|
||||||
|
|
||||||
// Submit button
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _canSubmit ? _submitAnswer : null,
|
|
||||||
icon: const Icon(Icons.check_circle),
|
|
||||||
label: const Text('Submit Answer'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: Size(200.w, 48.h),
|
|
||||||
textStyle: TextStyle(fontSize: 16.sp),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
SizedBox(height: spacingAfterColumns),
|
||||||
);
|
|
||||||
},
|
// Submit button
|
||||||
);
|
ElevatedButton.icon(
|
||||||
}
|
onPressed: _canSubmit ? _submitAnswer : null,
|
||||||
|
icon: const Icon(Icons.check_circle),
|
||||||
|
label: const Text('Submit Answer'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: Size(200.w, 48.h),
|
||||||
|
textStyle: TextStyle(fontSize: 16.sp),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildColumn(List<MatchItem> items, {required bool isLeft}) {
|
Widget _buildColumn(List<MatchItem> items, {required bool isLeft}) {
|
||||||
// Fixed sizes for consistent layout
|
// Fixed sizes for consistent layout
|
||||||
|
|
|
||||||
|
|
@ -181,11 +181,9 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Expanded(
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: size,
|
crossAxisCount: size,
|
||||||
|
|
@ -202,42 +200,42 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
||||||
),
|
),
|
||||||
SizedBox(height: spacingBetween),
|
SizedBox(height: spacingBetween),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: targetPaddingH,
|
horizontal: targetPaddingH,
|
||||||
vertical: targetPaddingV,
|
vertical: targetPaddingV,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.surface,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(24.r),
|
borderRadius: BorderRadius.circular(24.r),
|
||||||
border: Border.all(color: colorScheme.primary, width: 3),
|
border: Border.all(color: colorScheme.primary, width: 3),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: colorScheme.shadow.withOpacity(0.3),
|
color: colorScheme.shadow.withOpacity(0.3),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
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),
|
child: Column(
|
||||||
_buildTargetContent(context),
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,61 +80,77 @@ class QuestionDisplay extends StatelessWidget {
|
||||||
final spacingAfterImage = 12.h;
|
final spacingAfterImage = 12.h;
|
||||||
final spacingAfterText = 8.h;
|
final spacingAfterText = 8.h;
|
||||||
|
|
||||||
return Column(
|
return LayoutBuilder(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return SizedBox(
|
||||||
children: [
|
height: constraints.maxHeight.isFinite && constraints.maxHeight > 0
|
||||||
// Image display
|
? constraints.maxHeight
|
||||||
if (image != null) ...[
|
: null,
|
||||||
Flexible(
|
child: Column(
|
||||||
child: Image.network(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
image,
|
children: [
|
||||||
fit: BoxFit.contain,
|
// Image display
|
||||||
errorBuilder: (context, error, stackTrace) {
|
if (image != null) ...[
|
||||||
return Container(
|
Expanded(
|
||||||
height: 120.h,
|
flex: 3,
|
||||||
width: 120.w,
|
child: Center(
|
||||||
decoration: BoxDecoration(
|
child: Image.network(
|
||||||
color: Theme.of(
|
image,
|
||||||
context,
|
fit: BoxFit.contain,
|
||||||
).colorScheme.surfaceContainerHighest,
|
errorBuilder: (context, error, stackTrace) {
|
||||||
borderRadius: BorderRadius.circular(8.r),
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Icon(
|
),
|
||||||
Icons.image_not_supported,
|
SizedBox(height: spacingAfterImage),
|
||||||
size: 48.sp,
|
],
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
|
// Text display
|
||||||
|
if (text.isNotEmpty) ...[
|
||||||
|
Expanded(
|
||||||
|
flex: image != null ? 2 : 5,
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: theme.headlineSmall?.copyWith(
|
||||||
|
fontSize: 20.sp,
|
||||||
|
height: 1.4,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: null,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: spacingAfterImage),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Text display
|
// Audio button
|
||||||
if (text.isNotEmpty) ...[
|
if (audio != null) ...[
|
||||||
Flexible(
|
SizedBox(height: spacingAfterText),
|
||||||
child: Text(
|
_QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio),
|
||||||
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),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,21 @@ class CardImageUtils {
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a relative path (starts with /api/), convert to full URL
|
// If it's a relative path starting with /api/v2, extract base URL
|
||||||
|
// ApiConfigV2.baseUrl already contains /api/v2, so we need to remove it
|
||||||
|
if (imageUrl.startsWith('/api/v2/')) {
|
||||||
|
// Extract base URL without /api/v2
|
||||||
|
final baseUrl = ApiConfigV2.baseUrl;
|
||||||
|
final base = baseUrl.replaceAll('/api/v2', '');
|
||||||
|
return '$base$imageUrl';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a relative path starting with /, prepend base URL
|
||||||
if (imageUrl.startsWith('/')) {
|
if (imageUrl.startsWith('/')) {
|
||||||
return '${ApiConfigV2.baseUrl}$imageUrl';
|
return '${ApiConfigV2.baseUrl}$imageUrl';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: assume it's a relative path and prepend base URL
|
// Fallback: use helper method
|
||||||
return ApiConfigV2.getPackCoverUrl(pack.id);
|
return ApiConfigV2.getPackCoverUrl(pack.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue