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

This commit is contained in:
Dmitry 2025-12-20 23:28:04 +03:00
parent 82bc441b57
commit e7aab1d4ba
4 changed files with 255 additions and 89 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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) ||