stuff
Some checks failed
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
Backend CI / test (push) Has been cancelled
Backend CI / build (push) Has been cancelled
Some checks failed
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
Backend CI / test (push) Has been cancelled
Backend CI / build (push) Has been cancelled
This commit is contained in:
parent
82bc441b57
commit
e7aab1d4ba
4 changed files with 255 additions and 89 deletions
|
|
@ -169,10 +169,48 @@ class AdminTestsApiV2 {
|
||||||
if (v.isEmpty) return null;
|
if (v.isEmpty) return null;
|
||||||
|
|
||||||
// Already a URL: return as is
|
// Already a URL: return as is
|
||||||
if (CardImageStorage.isRemoteUrl(v) || CardImageStorage.isApiImageUrl(v)) {
|
if (CardImageStorage.isRemoteUrl(v)) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if path contains a full URL inside (malformed path like /api/.../cards/https://...)
|
||||||
|
// Extract the URL if found
|
||||||
|
final urlMatch = RegExp(r'https?://[^\s/]+[^\s]*').firstMatch(v);
|
||||||
|
if (urlMatch != null) {
|
||||||
|
var extractedUrl = urlMatch.group(0)!;
|
||||||
|
// Remove trailing /image if present
|
||||||
|
if (extractedUrl.endsWith('/image')) {
|
||||||
|
extractedUrl = extractedUrl.substring(0, extractedUrl.length - 6);
|
||||||
|
}
|
||||||
|
// Validate it's a proper URL
|
||||||
|
final uri = Uri.tryParse(extractedUrl);
|
||||||
|
if (uri != null &&
|
||||||
|
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||||
|
uri.hasAuthority) {
|
||||||
|
return extractedUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API URL: return as is (will be handled below)
|
||||||
|
if (CardImageStorage.isApiImageUrl(v)) {
|
||||||
|
// But first check if it contains a URL inside
|
||||||
|
// If so, extract it instead
|
||||||
|
final innerUrlMatch = RegExp(r'https?://[^\s/]+[^\s]*').firstMatch(v);
|
||||||
|
if (innerUrlMatch != null) {
|
||||||
|
var extractedUrl = innerUrlMatch.group(0)!;
|
||||||
|
if (extractedUrl.endsWith('/image')) {
|
||||||
|
extractedUrl = extractedUrl.substring(0, extractedUrl.length - 6);
|
||||||
|
}
|
||||||
|
final uri = Uri.tryParse(extractedUrl);
|
||||||
|
if (uri != null &&
|
||||||
|
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||||
|
uri.hasAuthority) {
|
||||||
|
return extractedUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, process as normal API URL
|
||||||
|
}
|
||||||
|
|
||||||
// If it's a valid UUID (object ID in MinIO), generate presigned URL
|
// If it's a valid UUID (object ID in MinIO), generate presigned URL
|
||||||
if (_isUuid(v)) {
|
if (_isUuid(v)) {
|
||||||
final presignedUrl = await _minioService.getPresignedUrl(
|
final presignedUrl = await _minioService.getPresignedUrl(
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,24 @@ class TestManager {
|
||||||
// Remote URL: keep as is
|
// Remote URL: keep as is
|
||||||
if (CardImageStorage.isRemoteUrl(v)) return v;
|
if (CardImageStorage.isRemoteUrl(v)) return v;
|
||||||
|
|
||||||
|
// Check if path contains a full URL inside (malformed path like /api/.../cards/https://...)
|
||||||
|
// Extract the URL if found
|
||||||
|
final urlMatch = RegExp(r'https?://[^\s/]+[^\s]*').firstMatch(v);
|
||||||
|
if (urlMatch != null) {
|
||||||
|
final extractedUrl = urlMatch.group(0)!;
|
||||||
|
// Remove trailing /image if present
|
||||||
|
final cleanUrl = extractedUrl.endsWith('/image')
|
||||||
|
? extractedUrl.substring(0, extractedUrl.length - 6)
|
||||||
|
: extractedUrl;
|
||||||
|
// Validate it's a proper URL
|
||||||
|
final uri = Uri.tryParse(cleanUrl);
|
||||||
|
if (uri != null &&
|
||||||
|
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||||
|
uri.hasAuthority) {
|
||||||
|
return cleanUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If it's already a valid UUID (object ID in MinIO), keep it
|
// If it's already a valid UUID (object ID in MinIO), keep it
|
||||||
if (_isUuid(v)) {
|
if (_isUuid(v)) {
|
||||||
return v;
|
return v;
|
||||||
|
|
@ -151,10 +169,48 @@ class TestManager {
|
||||||
if (v.isEmpty) return null;
|
if (v.isEmpty) return null;
|
||||||
|
|
||||||
// Already a URL: return as is
|
// Already a URL: return as is
|
||||||
if (CardImageStorage.isRemoteUrl(v) || CardImageStorage.isApiImageUrl(v)) {
|
if (CardImageStorage.isRemoteUrl(v)) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if path contains a full URL inside (malformed path like /api/.../cards/https://...)
|
||||||
|
// Extract the URL if found
|
||||||
|
final urlMatch = RegExp(r'https?://[^\s/]+[^\s]*').firstMatch(v);
|
||||||
|
if (urlMatch != null) {
|
||||||
|
var extractedUrl = urlMatch.group(0)!;
|
||||||
|
// Remove trailing /image if present
|
||||||
|
if (extractedUrl.endsWith('/image')) {
|
||||||
|
extractedUrl = extractedUrl.substring(0, extractedUrl.length - 6);
|
||||||
|
}
|
||||||
|
// Validate it's a proper URL
|
||||||
|
final uri = Uri.tryParse(extractedUrl);
|
||||||
|
if (uri != null &&
|
||||||
|
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||||
|
uri.hasAuthority) {
|
||||||
|
return extractedUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API URL: check if it contains a URL inside
|
||||||
|
if (CardImageStorage.isApiImageUrl(v)) {
|
||||||
|
// But first check if it contains a URL inside
|
||||||
|
// If so, extract it instead
|
||||||
|
final innerUrlMatch = RegExp(r'https?://[^\s/]+[^\s]*').firstMatch(v);
|
||||||
|
if (innerUrlMatch != null) {
|
||||||
|
var extractedUrl = innerUrlMatch.group(0)!;
|
||||||
|
if (extractedUrl.endsWith('/image')) {
|
||||||
|
extractedUrl = extractedUrl.substring(0, extractedUrl.length - 6);
|
||||||
|
}
|
||||||
|
final uri = Uri.tryParse(extractedUrl);
|
||||||
|
if (uri != null &&
|
||||||
|
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||||
|
uri.hasAuthority) {
|
||||||
|
return extractedUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, process as normal API URL (will be handled below)
|
||||||
|
}
|
||||||
|
|
||||||
// If it's a valid UUID (object ID in MinIO), generate presigned URL
|
// If it's a valid UUID (object ID in MinIO), generate presigned URL
|
||||||
if (_isUuid(v)) {
|
if (_isUuid(v)) {
|
||||||
final presignedUrl = await _minioService.getPresignedUrl(
|
final presignedUrl = await _minioService.getPresignedUrl(
|
||||||
|
|
|
||||||
|
|
@ -315,46 +315,74 @@ class _GamePageState extends State<GamePage> {
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 150.h,
|
minHeight: 150.h,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: currentQuestion is GameQuestionMatrix
|
||||||
child: AnimatedSwitcher(
|
? AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: KeyedSubtree(
|
child: KeyedSubtree(
|
||||||
key: questionKey,
|
key: questionKey,
|
||||||
child: Material(
|
child: Material(
|
||||||
key: GamePage.questionCardKey,
|
key: GamePage.questionCardKey,
|
||||||
color: colorScheme.surface,
|
color: colorScheme.surface,
|
||||||
surfaceTintColor: colorScheme.surfaceTint,
|
surfaceTintColor: colorScheme.surfaceTint,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shadowColor: theme.shadowColor.withOpacity(
|
shadowColor: theme.shadowColor.withOpacity(
|
||||||
theme.brightness == Brightness.dark
|
theme.brightness == Brightness.dark
|
||||||
? 0.35
|
? 0.35
|
||||||
: 0.14,
|
: 0.14,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(18.r),
|
borderRadius: BorderRadius.circular(18.r),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: colorScheme.outlineVariant,
|
color: colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(
|
||||||
|
isNarrow ? 14.w : 18.w,
|
||||||
|
),
|
||||||
|
child: MatrixWidget(
|
||||||
|
question: currentQuestion.question,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
)
|
||||||
padding: EdgeInsets.all(
|
: SingleChildScrollView(
|
||||||
isNarrow ? 14.w : 18.w,
|
child: AnimatedSwitcher(
|
||||||
),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: currentQuestion is GameQuestionMatrix
|
child: KeyedSubtree(
|
||||||
? MatrixWidget(
|
key: questionKey,
|
||||||
question: currentQuestion.question,
|
child: Material(
|
||||||
)
|
key: GamePage.questionCardKey,
|
||||||
: QuestionDisplay(
|
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: QuestionDisplay(
|
||||||
question: currentQuestion,
|
question: currentQuestion,
|
||||||
onPlayAudio:
|
onPlayAudio:
|
||||||
widget.questionAudioPlayback ??
|
widget.questionAudioPlayback ??
|
||||||
_playQuestionAudio,
|
_playQuestionAudio,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -367,8 +395,43 @@ class _GamePageState extends State<GamePage> {
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 200.h,
|
minHeight: 200.h,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: currentQuestion.when(
|
||||||
child: AnimatedSwitcher(
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputLetters: (q) => AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 400),
|
duration: const Duration(milliseconds: 400),
|
||||||
switchInCurve: Curves.easeInOut,
|
switchInCurve: Curves.easeInOut,
|
||||||
switchOutCurve: Curves.easeInOut,
|
switchOutCurve: Curves.easeInOut,
|
||||||
|
|
@ -388,28 +451,37 @@ class _GamePageState extends State<GamePage> {
|
||||||
key: ValueKey(
|
key: ValueKey(
|
||||||
'question_input_${currentQuestion.hashCode}',
|
'question_input_${currentQuestion.hashCode}',
|
||||||
),
|
),
|
||||||
child: currentQuestion.when(
|
child: InputLettersWidget(
|
||||||
multipleChoice: (q) => AnswerOptions(
|
question: q,
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
match: (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: MatchWidget(
|
||||||
|
question: q,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
matrix: (q) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -200,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(
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue