fixes
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
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
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
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
de06c74b72
commit
9c2456f685
9 changed files with 133 additions and 94 deletions
|
|
@ -456,6 +456,17 @@ class AdminCardsApiV2 {
|
|||
);
|
||||
await _db.packDao.updateCard(updated);
|
||||
|
||||
// Обновить связь с паком
|
||||
if (requestDto.packId != null && requestDto.packId!.isNotEmpty) {
|
||||
// Сначала удалить старую связь
|
||||
await _db.packDao.removeCardFromPack(updated.id, existing.packId ?? '');
|
||||
// Затем добавить новую
|
||||
await _db.packDao.addCardToPack(cardId: updated.id, packId: requestDto.packId!);
|
||||
} else {
|
||||
// Если packId не указан или пустой, удалить связь
|
||||
await _db.packDao.removeCardFromPack(updated.id, existing.packId ?? '');
|
||||
}
|
||||
|
||||
// Получить паки для карточки
|
||||
final packs = await _db.packDao.getPacksForCard(updated.id);
|
||||
final packId = packs.isNotEmpty ? packs.first.id : null;
|
||||
|
|
@ -501,7 +512,7 @@ class AdminCardsApiV2 {
|
|||
final cardId = await _db.packDao.createCard(companion);
|
||||
|
||||
// Если передан packId, создать связь через CardPackCards
|
||||
if (requestDto.packId != null) {
|
||||
if (requestDto.packId != null && requestDto.packId!.isNotEmpty) {
|
||||
await _db.packDao.addCardToPack(cardId: cardId, packId: requestDto.packId!);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class AdminTestsApiV2 {
|
|||
final cardId = await _db.packDao.createCard(companion);
|
||||
|
||||
// If packId is provided, link card to pack
|
||||
if (packId != null) {
|
||||
if (packId != null && packId.isNotEmpty) {
|
||||
await _db.packDao.addCardToPack(cardId: cardId, packId: packId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -553,32 +553,13 @@ class TestManager {
|
|||
|
||||
final tests = await _db.testDao.getTestsByPackId(packId);
|
||||
|
||||
// Helper function to convert image ID to URL
|
||||
String? _convertImageToUrl(String? imageValue, String? packId) {
|
||||
if (imageValue == null || packId == null) return imageValue;
|
||||
|
||||
// If it's already a proper URL, return as is
|
||||
if (imageValue.startsWith('http://') ||
|
||||
imageValue.startsWith('https://') ||
|
||||
(imageValue.startsWith('/api/') && imageValue.contains('/cards/') && imageValue.endsWith('/image'))) {
|
||||
return imageValue;
|
||||
}
|
||||
|
||||
// If it looks like a UUID (card ID), convert to URL
|
||||
if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) {
|
||||
return '/api/v2/packs/$packId/cards/$imageValue/image';
|
||||
}
|
||||
|
||||
// Otherwise, assume it's already a card ID and convert
|
||||
return '/api/v2/packs/$packId/cards/$imageValue/image';
|
||||
}
|
||||
|
||||
final testDtos = <TestDto>[];
|
||||
for (final test in tests) {
|
||||
final questions = await _db.testDao.getTestQuestions(test.id);
|
||||
final statistics = await _testStatisticsDto(user.id!, test.id);
|
||||
|
||||
final questionsList = questions.map((q) {
|
||||
final questionsList = await Future.wait(
|
||||
questions.map((q) async {
|
||||
// Build question JSON from separate fields
|
||||
final questionJson = <String, dynamic>{
|
||||
'questionType': q.questionType,
|
||||
|
|
@ -594,20 +575,30 @@ class TestManager {
|
|||
buttons = [];
|
||||
}
|
||||
|
||||
// Convert button images to URLs
|
||||
final buttonsWithUrls = buttons.map((button) {
|
||||
// Convert button images to URLs (works for both TestButtonDto and matrix cards)
|
||||
// Add imageUrl while keeping image (objectId) for admin
|
||||
final updatedButtons = await Future.wait(
|
||||
buttons.map((button) async {
|
||||
if (button is Map<String, dynamic> && button['image'] != null) {
|
||||
final buttonMap = Map<String, dynamic>.from(button);
|
||||
buttonMap['image'] = _convertImageToUrl(
|
||||
buttonMap['image'] as String?,
|
||||
packId,
|
||||
final imageValue = buttonMap['image']?.toString();
|
||||
|
||||
// Convert image to presigned URL
|
||||
final imageUrl = await _imageValueToApiUrl(
|
||||
imageValue,
|
||||
packId: packId,
|
||||
);
|
||||
|
||||
if (imageUrl != null) {
|
||||
buttonMap['imageUrl'] = imageUrl;
|
||||
}
|
||||
return buttonMap;
|
||||
}
|
||||
return button;
|
||||
}).toList();
|
||||
}),
|
||||
);
|
||||
|
||||
questionJson['buttons'] = buttonsWithUrls;
|
||||
questionJson['buttons'] = updatedButtons;
|
||||
|
||||
// Add answer
|
||||
questionJson['answer'] = q.answer;
|
||||
|
|
@ -617,10 +608,14 @@ class TestManager {
|
|||
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
||||
// Convert question image to URL
|
||||
if (uiData['image'] != null) {
|
||||
uiData['image'] = _convertImageToUrl(
|
||||
uiData['image'] as String?,
|
||||
packId,
|
||||
final imageValue = uiData['image']?.toString();
|
||||
final imageUrl = await _imageValueToApiUrl(
|
||||
imageValue,
|
||||
packId: packId,
|
||||
);
|
||||
if (imageUrl != null) {
|
||||
uiData['imageUrl'] = imageUrl;
|
||||
}
|
||||
}
|
||||
questionJson.addAll(uiData);
|
||||
} catch (e) {
|
||||
|
|
@ -628,13 +623,22 @@ class TestManager {
|
|||
}
|
||||
|
||||
return AbstractTestQuestion.fromJson(questionJson);
|
||||
}).toList();
|
||||
}),
|
||||
);
|
||||
|
||||
final normalizedCover =
|
||||
await _normalizeImageValueForDb(test.cover, packId: packId);
|
||||
final coverUrl = await _imageValueToApiUrl(
|
||||
normalizedCover,
|
||||
packId: packId,
|
||||
);
|
||||
|
||||
testDtos.add(TestDto(
|
||||
id: test.id.toString(),
|
||||
name: test.name,
|
||||
color: test.color,
|
||||
cover: test.cover,
|
||||
cover: normalizedCover, // Object ID (for admin)
|
||||
coverUrl: coverUrl, // Presigned URL (for display)
|
||||
version: test.version ?? '1.0',
|
||||
time: test.time,
|
||||
timeSubtitle: test.timeSubtitle,
|
||||
|
|
|
|||
|
|
@ -295,6 +295,20 @@ class TestsStateManager extends StateManager<TestsState> {
|
|||
if (currentState is! _GameSessionActive || _currentTest == null) return;
|
||||
|
||||
try {
|
||||
// Check if current question has been answered
|
||||
final currentQuestion = currentState.questions[currentState.currentQuestionIndex];
|
||||
final questionId = _getQuestionId(currentQuestion);
|
||||
final existingResult = _gameSessionManager.getQuestionResult(questionId);
|
||||
|
||||
// If current question hasn't been answered, save it as unanswered
|
||||
if (existingResult == null || existingResult.answeredAt == null) {
|
||||
log(
|
||||
'Current question not answered, saving as unanswered before completion',
|
||||
name: 'TestsStateManager',
|
||||
);
|
||||
_gameSessionManager.submitAnswer(questionId, currentQuestion, '');
|
||||
}
|
||||
|
||||
final result = _gameSessionManager.completeSession(_currentTest!.id!);
|
||||
|
||||
// Play completion sound
|
||||
|
|
|
|||
|
|
@ -544,10 +544,15 @@ class _GamePageState extends State<GamePage> {
|
|||
|
||||
|
||||
Future<void> _finishGame() async {
|
||||
log('Finish button pressed', name: 'GamePage');
|
||||
final appScope = ScopeProvider.of<AppScopeContainer>(context, listen: false);
|
||||
final userScope = appScope?.userScopeHolder.scope;
|
||||
if (userScope != null) {
|
||||
log('Completing game session...', name: 'GamePage');
|
||||
await userScope.testsModule.testsStateManager.completeGameSession();
|
||||
log('Game session completion called', name: 'GamePage');
|
||||
} else {
|
||||
log('No user scope available when finishing game', name: 'GamePage');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,10 +48,6 @@ class CardFavoriteButton extends StatelessWidget {
|
|||
),
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface.withOpacity(0.9),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFavorite ? Colors.red : colorScheme.onSurface,
|
||||
|
|
|
|||
|
|
@ -432,6 +432,7 @@ class _CardSide extends StatelessWidget {
|
|||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Верхняя секция: Original + Translation + Voice Controls
|
||||
Container(
|
||||
|
|
|
|||
|
|
@ -102,6 +102,17 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (widget.question.word.isNotEmpty) ...[
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
widget.question.word,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
SizedBox(height: 16.h),
|
||||
_buildTemplateDisplay(),
|
||||
],
|
||||
|
|
@ -166,10 +177,9 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
final template = widget.question.template;
|
||||
final currentInput = _currentAnswer;
|
||||
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8.w,
|
||||
runSpacing: 8.h,
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildTemplateParts(template, currentInput),
|
||||
);
|
||||
}
|
||||
|
|
@ -188,8 +198,9 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
|
||||
parts.add(
|
||||
Container(
|
||||
width: 40.w,
|
||||
width: 32.w,
|
||||
height: 48.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: 2.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
|
|
@ -207,16 +218,23 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (char == ' ') {
|
||||
// Space character - add small spacing
|
||||
parts.add(
|
||||
SizedBox(width: 8.w),
|
||||
);
|
||||
} else {
|
||||
// This is a regular character
|
||||
parts.add(
|
||||
Container(
|
||||
width: 32.w,
|
||||
width: 28.w,
|
||||
height: 48.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: 2.w),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
char,
|
||||
|
|
@ -224,6 +242,7 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
|||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -59,17 +59,6 @@ class PackCardItem extends StatelessWidget {
|
|||
child: Container(
|
||||
key: ValueKey('$favoriteRootKeyPrefix${card.id}'),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface.withOpacity(0.9),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withOpacity(0.2),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||
size: 14,
|
||||
|
|
|
|||
Loading…
Reference in a new issue