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;
|
||||
|
||||
// Already a URL: return as is
|
||||
if (CardImageStorage.isRemoteUrl(v) || CardImageStorage.isApiImageUrl(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) {
|
||||
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 (_isUuid(v)) {
|
||||
final presignedUrl = await _minioService.getPresignedUrl(
|
||||
|
|
|
|||
|
|
@ -107,6 +107,24 @@ class TestManager {
|
|||
// Remote URL: keep as is
|
||||
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 (_isUuid(v)) {
|
||||
return v;
|
||||
|
|
@ -151,10 +169,48 @@ class TestManager {
|
|||
if (v.isEmpty) return null;
|
||||
|
||||
// Already a URL: return as is
|
||||
if (CardImageStorage.isRemoteUrl(v) || CardImageStorage.isApiImageUrl(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) {
|
||||
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 (_isUuid(v)) {
|
||||
final presignedUrl = await _minioService.getPresignedUrl(
|
||||
|
|
|
|||
|
|
@ -315,7 +315,39 @@ class _GamePageState extends State<GamePage> {
|
|||
constraints: BoxConstraints(
|
||||
minHeight: 150.h,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: currentQuestion is GameQuestionMatrix
|
||||
? 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: MatrixWidget(
|
||||
question: currentQuestion.question,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: KeyedSubtree(
|
||||
|
|
@ -340,11 +372,7 @@ class _GamePageState extends State<GamePage> {
|
|||
padding: EdgeInsets.all(
|
||||
isNarrow ? 14.w : 18.w,
|
||||
),
|
||||
child: currentQuestion is GameQuestionMatrix
|
||||
? MatrixWidget(
|
||||
question: currentQuestion.question,
|
||||
)
|
||||
: QuestionDisplay(
|
||||
child: QuestionDisplay(
|
||||
question: currentQuestion,
|
||||
onPlayAudio:
|
||||
widget.questionAudioPlayback ??
|
||||
|
|
@ -367,7 +395,8 @@ class _GamePageState extends State<GamePage> {
|
|||
constraints: BoxConstraints(
|
||||
minHeight: 200.h,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: currentQuestion.when(
|
||||
multipleChoice: (q) => SingleChildScrollView(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
|
|
@ -388,8 +417,7 @@ class _GamePageState extends State<GamePage> {
|
|||
key: ValueKey(
|
||||
'question_input_${currentQuestion.hashCode}',
|
||||
),
|
||||
child: currentQuestion.when(
|
||||
multipleChoice: (q) => AnswerOptions(
|
||||
child: AnswerOptions(
|
||||
question: q,
|
||||
selectedAnswer:
|
||||
_getSelectedAnswerForMultipleChoice(
|
||||
|
|
@ -400,19 +428,63 @@ class _GamePageState extends State<GamePage> {
|
|||
isAnswerSubmitted: isAnswerSubmitted,
|
||||
isCorrect: isCorrect,
|
||||
),
|
||||
inputLetters: (q) => InputLettersWidget(
|
||||
),
|
||||
),
|
||||
),
|
||||
inputLetters: (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: InputLettersWidget(
|
||||
question: q,
|
||||
),
|
||||
match: (q) => MatchWidget(
|
||||
),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Navigation buttons (show when navigation is possible)
|
||||
if (_canGoPrevious(state) ||
|
||||
|
|
|
|||
Loading…
Reference in a new issue