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(
|
||||
children: [
|
||||
// Question display
|
||||
Flexible(
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: KeyedSubtree(
|
||||
key: questionKey,
|
||||
child: Material(
|
||||
key: GamePage.questionCardKey,
|
||||
color: colorScheme.surface,
|
||||
surfaceTintColor: colorScheme.surfaceTint,
|
||||
elevation: 3,
|
||||
shadowColor: theme.shadowColor.withOpacity(
|
||||
theme.brightness == Brightness.dark
|
||||
? 0.35
|
||||
: 0.14,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 150.h,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: KeyedSubtree(
|
||||
key: questionKey,
|
||||
child: Material(
|
||||
key: GamePage.questionCardKey,
|
||||
color: colorScheme.surface,
|
||||
surfaceTintColor: colorScheme.surfaceTint,
|
||||
elevation: 3,
|
||||
shadowColor: theme.shadowColor.withOpacity(
|
||||
theme.brightness == Brightness.dark
|
||||
? 0.35
|
||||
: 0.14,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
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),
|
||||
|
||||
// Answer input based on question type with smooth transitions
|
||||
Flexible(
|
||||
Expanded(
|
||||
flex: 3,
|
||||
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: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 200.h,
|
||||
),
|
||||
child: 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: 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 &&
|
||||
question.optionItems.any((item) => item.image != null);
|
||||
|
||||
// Fixed aspect ratios for consistent layout
|
||||
final childAspectRatio = hasImages ? 1.1 : 4.0;
|
||||
final spacing = 12.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);
|
||||
}
|
||||
},
|
||||
// Calculate childAspectRatio based on available height to fill space
|
||||
// If we have available height, use it to calculate aspect ratio
|
||||
double childAspectRatio;
|
||||
if (constraints.maxHeight.isFinite && constraints.maxHeight > 0) {
|
||||
// Calculate how many rows we need
|
||||
final rows = (itemCount / crossAxisCount).ceil();
|
||||
// Calculate height per item (accounting for spacing)
|
||||
final totalSpacing = (rows - 1) * spacing;
|
||||
final availableHeightForItems = constraints.maxHeight - totalSpacing;
|
||||
final itemHeight = availableHeightForItems / rows;
|
||||
// Aspect ratio = width / height
|
||||
final itemWidth = constraints.maxWidth / crossAxisCount - spacing;
|
||||
childAspectRatio = itemWidth / itemHeight;
|
||||
// Clamp to reasonable values
|
||||
childAspectRatio = childAspectRatio.clamp(
|
||||
hasImages ? 0.8 : 2.0,
|
||||
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) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Display the template with current input
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(containerPadding),
|
||||
margin: EdgeInsets.only(bottom: containerMargin),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Fill in the blanks:',
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (widget.question.word.isNotEmpty)
|
||||
SizedBox(height: spacingAfterTitle),
|
||||
if (widget.question.word.isNotEmpty) ...[
|
||||
Text(
|
||||
widget.question.word,
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
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),
|
||||
),
|
||||
],
|
||||
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)
|
||||
if (widget.question.buttons.isNotEmpty) ...[
|
||||
Flexible(child: _buildButtonsGrid(constraints)),
|
||||
SizedBox(height: spacingAfterGrid),
|
||||
] else ...[
|
||||
// Input field (only if no buttons)
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.8,
|
||||
),
|
||||
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,
|
||||
),
|
||||
// Buttons with images or text (if available)
|
||||
if (widget.question.buttons.isNotEmpty) ...[
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildButtonsGrid(constraints),
|
||||
),
|
||||
SizedBox(height: spacingAfterGrid),
|
||||
] else ...[
|
||||
// Input field (only if no buttons)
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.8,
|
||||
),
|
||||
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
|
||||
ElevatedButton.icon(
|
||||
|
|
@ -276,7 +281,6 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
final spacing = 12.0;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ class _MatchWidgetState extends State<MatchWidget> {
|
|||
final isWideScreen = constraints.maxWidth > 600;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Instructions
|
||||
Container(
|
||||
|
|
@ -86,84 +85,82 @@ class _MatchWidgetState extends State<MatchWidget> {
|
|||
],
|
||||
|
||||
// Two columns layout
|
||||
Flexible(
|
||||
child: isWideScreen
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildColumn(
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: isWideScreen
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildColumn(
|
||||
widget.question.leftItems,
|
||||
isLeft: true,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 24.w),
|
||||
Expanded(
|
||||
child: _buildColumn(
|
||||
widget.question.rightItems,
|
||||
isLeft: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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,
|
||||
isLeft: true,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 24.w),
|
||||
Expanded(
|
||||
child: _buildColumn(
|
||||
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),
|
||||
_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),
|
||||
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}) {
|
||||
// Fixed sizes for consistent layout
|
||||
|
|
|
|||
|
|
@ -181,11 +181,9 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
|||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: size,
|
||||
|
|
@ -202,42 +200,42 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
|||
),
|
||||
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,
|
||||
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),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,61 +80,77 @@ class QuestionDisplay extends StatelessWidget {
|
|||
final spacingAfterImage = 12.h;
|
||||
final spacingAfterText = 8.h;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Image display
|
||||
if (image != null) ...[
|
||||
Flexible(
|
||||
child: Image.network(
|
||||
image,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 120.h,
|
||||
width: 120.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
height: constraints.maxHeight.isFinite && constraints.maxHeight > 0
|
||||
? constraints.maxHeight
|
||||
: null,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Image display
|
||||
if (image != null) ...[
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Center(
|
||||
child: Image.network(
|
||||
image,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 120.h,
|
||||
width: 120.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48.sp,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48.sp,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
SizedBox(height: spacingAfterImage),
|
||||
],
|
||||
|
||||
// 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
|
||||
if (text.isNotEmpty) ...[
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.headlineSmall?.copyWith(
|
||||
fontSize: 20.sp,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
// Audio button
|
||||
if (audio != null) ...[
|
||||
SizedBox(height: spacingAfterText),
|
||||
_QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Audio button
|
||||
if (audio != null) ...[
|
||||
SizedBox(height: spacingAfterText),
|
||||
_QuestionAudioButton(audioUrl: audio, onPlayAudio: onPlayAudio),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,12 +79,21 @@ class CardImageUtils {
|
|||
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('/')) {
|
||||
return '${ApiConfigV2.baseUrl}$imageUrl';
|
||||
}
|
||||
|
||||
// Fallback: assume it's a relative path and prepend base URL
|
||||
// Fallback: use helper method
|
||||
return ApiConfigV2.getPackCoverUrl(pack.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue