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
|
|
@ -455,7 +455,18 @@ class AdminCardsApiV2 {
|
||||||
updatedAt: PgDateTime(DateTime.now()),
|
updatedAt: PgDateTime(DateTime.now()),
|
||||||
);
|
);
|
||||||
await _db.packDao.updateCard(updated);
|
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 packs = await _db.packDao.getPacksForCard(updated.id);
|
||||||
final packId = packs.isNotEmpty ? packs.first.id : null;
|
final packId = packs.isNotEmpty ? packs.first.id : null;
|
||||||
|
|
@ -501,7 +512,7 @@ class AdminCardsApiV2 {
|
||||||
final cardId = await _db.packDao.createCard(companion);
|
final cardId = await _db.packDao.createCard(companion);
|
||||||
|
|
||||||
// Если передан packId, создать связь через CardPackCards
|
// Если передан packId, создать связь через CardPackCards
|
||||||
if (requestDto.packId != null) {
|
if (requestDto.packId != null && requestDto.packId!.isNotEmpty) {
|
||||||
await _db.packDao.addCardToPack(cardId: cardId, packId: requestDto.packId!);
|
await _db.packDao.addCardToPack(cardId: cardId, packId: requestDto.packId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class AdminTestsApiV2 {
|
||||||
final cardId = await _db.packDao.createCard(companion);
|
final cardId = await _db.packDao.createCard(companion);
|
||||||
|
|
||||||
// If packId is provided, link card to pack
|
// If packId is provided, link card to pack
|
||||||
if (packId != null) {
|
if (packId != null && packId.isNotEmpty) {
|
||||||
await _db.packDao.addCardToPack(cardId: cardId, packId: packId);
|
await _db.packDao.addCardToPack(cardId: cardId, packId: packId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -553,88 +553,92 @@ class TestManager {
|
||||||
|
|
||||||
final tests = await _db.testDao.getTestsByPackId(packId);
|
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>[];
|
final testDtos = <TestDto>[];
|
||||||
for (final test in tests) {
|
for (final test in tests) {
|
||||||
final questions = await _db.testDao.getTestQuestions(test.id);
|
final questions = await _db.testDao.getTestQuestions(test.id);
|
||||||
final statistics = await _testStatisticsDto(user.id!, test.id);
|
final statistics = await _testStatisticsDto(user.id!, test.id);
|
||||||
|
|
||||||
final questionsList = questions.map((q) {
|
final questionsList = await Future.wait(
|
||||||
// Build question JSON from separate fields
|
questions.map((q) async {
|
||||||
final questionJson = <String, dynamic>{
|
// Build question JSON from separate fields
|
||||||
'questionType': q.questionType,
|
final questionJson = <String, dynamic>{
|
||||||
'id': q.id,
|
'questionType': q.questionType,
|
||||||
'word': q.word,
|
'id': q.id,
|
||||||
};
|
'word': q.word,
|
||||||
|
};
|
||||||
// Parse options (JSON array of buttons)
|
|
||||||
List<dynamic> buttons = [];
|
// Parse options (JSON array of buttons)
|
||||||
try {
|
List<dynamic> buttons = [];
|
||||||
buttons = json.decode(q.options) as List<dynamic>;
|
try {
|
||||||
} catch (e) {
|
buttons = json.decode(q.options) as List<dynamic>;
|
||||||
buttons = [];
|
} catch (e) {
|
||||||
}
|
buttons = [];
|
||||||
|
|
||||||
// Convert button images to URLs
|
|
||||||
final buttonsWithUrls = buttons.map((button) {
|
|
||||||
if (button is Map<String, dynamic> && button['image'] != null) {
|
|
||||||
final buttonMap = Map<String, dynamic>.from(button);
|
|
||||||
buttonMap['image'] = _convertImageToUrl(
|
|
||||||
buttonMap['image'] as String?,
|
|
||||||
packId,
|
|
||||||
);
|
|
||||||
return buttonMap;
|
|
||||||
}
|
}
|
||||||
return button;
|
|
||||||
}).toList();
|
// Convert button images to URLs (works for both TestButtonDto and matrix cards)
|
||||||
|
// Add imageUrl while keeping image (objectId) for admin
|
||||||
questionJson['buttons'] = buttonsWithUrls;
|
final updatedButtons = await Future.wait(
|
||||||
|
buttons.map((button) async {
|
||||||
// Add answer
|
if (button is Map<String, dynamic> && button['image'] != null) {
|
||||||
questionJson['answer'] = q.answer;
|
final buttonMap = Map<String, dynamic>.from(button);
|
||||||
|
final imageValue = buttonMap['image']?.toString();
|
||||||
// Parse uiData (image, text, audio, template)
|
|
||||||
try {
|
// Convert image to presigned URL
|
||||||
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
final imageUrl = await _imageValueToApiUrl(
|
||||||
// Convert question image to URL
|
imageValue,
|
||||||
if (uiData['image'] != null) {
|
packId: packId,
|
||||||
uiData['image'] = _convertImageToUrl(
|
);
|
||||||
uiData['image'] as String?,
|
|
||||||
packId,
|
if (imageUrl != null) {
|
||||||
);
|
buttonMap['imageUrl'] = imageUrl;
|
||||||
|
}
|
||||||
|
return buttonMap;
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
questionJson['buttons'] = updatedButtons;
|
||||||
|
|
||||||
|
// Add answer
|
||||||
|
questionJson['answer'] = q.answer;
|
||||||
|
|
||||||
|
// Parse uiData (image, text, audio, template)
|
||||||
|
try {
|
||||||
|
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
|
||||||
|
// Convert question image to URL
|
||||||
|
if (uiData['image'] != null) {
|
||||||
|
final imageValue = uiData['image']?.toString();
|
||||||
|
final imageUrl = await _imageValueToApiUrl(
|
||||||
|
imageValue,
|
||||||
|
packId: packId,
|
||||||
|
);
|
||||||
|
if (imageUrl != null) {
|
||||||
|
uiData['imageUrl'] = imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
questionJson.addAll(uiData);
|
||||||
|
} catch (e) {
|
||||||
|
// If uiData is empty or invalid, ignore
|
||||||
}
|
}
|
||||||
questionJson.addAll(uiData);
|
|
||||||
} catch (e) {
|
return AbstractTestQuestion.fromJson(questionJson);
|
||||||
// If uiData is empty or invalid, ignore
|
}),
|
||||||
}
|
);
|
||||||
|
|
||||||
return AbstractTestQuestion.fromJson(questionJson);
|
final normalizedCover =
|
||||||
}).toList();
|
await _normalizeImageValueForDb(test.cover, packId: packId);
|
||||||
|
final coverUrl = await _imageValueToApiUrl(
|
||||||
|
normalizedCover,
|
||||||
|
packId: packId,
|
||||||
|
);
|
||||||
|
|
||||||
testDtos.add(TestDto(
|
testDtos.add(TestDto(
|
||||||
id: test.id.toString(),
|
id: test.id.toString(),
|
||||||
name: test.name,
|
name: test.name,
|
||||||
color: test.color,
|
color: test.color,
|
||||||
cover: test.cover,
|
cover: normalizedCover, // Object ID (for admin)
|
||||||
|
coverUrl: coverUrl, // Presigned URL (for display)
|
||||||
version: test.version ?? '1.0',
|
version: test.version ?? '1.0',
|
||||||
time: test.time,
|
time: test.time,
|
||||||
timeSubtitle: test.timeSubtitle,
|
timeSubtitle: test.timeSubtitle,
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,20 @@ class TestsStateManager extends StateManager<TestsState> {
|
||||||
if (currentState is! _GameSessionActive || _currentTest == null) return;
|
if (currentState is! _GameSessionActive || _currentTest == null) return;
|
||||||
|
|
||||||
try {
|
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!);
|
final result = _gameSessionManager.completeSession(_currentTest!.id!);
|
||||||
|
|
||||||
// Play completion sound
|
// Play completion sound
|
||||||
|
|
|
||||||
|
|
@ -544,10 +544,15 @@ class _GamePageState extends State<GamePage> {
|
||||||
|
|
||||||
|
|
||||||
Future<void> _finishGame() async {
|
Future<void> _finishGame() async {
|
||||||
|
log('Finish button pressed', name: 'GamePage');
|
||||||
final appScope = ScopeProvider.of<AppScopeContainer>(context, listen: false);
|
final appScope = ScopeProvider.of<AppScopeContainer>(context, listen: false);
|
||||||
final userScope = appScope?.userScopeHolder.scope;
|
final userScope = appScope?.userScopeHolder.scope;
|
||||||
if (userScope != null) {
|
if (userScope != null) {
|
||||||
|
log('Completing game session...', name: 'GamePage');
|
||||||
await userScope.testsModule.testsStateManager.completeGameSession();
|
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(
|
icon: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colorScheme.surface.withOpacity(0.9),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||||
color: isFavorite ? Colors.red : colorScheme.onSurface,
|
color: isFavorite ? Colors.red : colorScheme.onSurface,
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,7 @@ class _CardSide extends StatelessWidget {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Верхняя секция: Original + Translation + Voice Controls
|
// Верхняя секция: Original + Translation + Voice Controls
|
||||||
Container(
|
Container(
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,17 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
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),
|
SizedBox(height: 16.h),
|
||||||
_buildTemplateDisplay(),
|
_buildTemplateDisplay(),
|
||||||
],
|
],
|
||||||
|
|
@ -166,10 +177,9 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
final template = widget.question.template;
|
final template = widget.question.template;
|
||||||
final currentInput = _currentAnswer;
|
final currentInput = _currentAnswer;
|
||||||
|
|
||||||
return Wrap(
|
return Row(
|
||||||
alignment: WrapAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
spacing: 8.w,
|
mainAxisSize: MainAxisSize.min,
|
||||||
runSpacing: 8.h,
|
|
||||||
children: _buildTemplateParts(template, currentInput),
|
children: _buildTemplateParts(template, currentInput),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -188,8 +198,9 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
|
|
||||||
parts.add(
|
parts.add(
|
||||||
Container(
|
Container(
|
||||||
width: 40.w,
|
width: 32.w,
|
||||||
height: 48.h,
|
height: 48.h,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 2.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
|
@ -207,16 +218,23 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
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 {
|
} else {
|
||||||
// This is a regular character
|
// This is a regular character
|
||||||
parts.add(
|
parts.add(
|
||||||
Container(
|
Container(
|
||||||
width: 32.w,
|
width: 28.w,
|
||||||
height: 48.h,
|
height: 48.h,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 2.w),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
char,
|
char,
|
||||||
|
|
@ -224,6 +242,7 @@ class _InputLettersWidgetState extends State<InputLettersWidget> {
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -59,17 +59,6 @@ class PackCardItem extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
key: ValueKey('$favoriteRootKeyPrefix${card.id}'),
|
key: ValueKey('$favoriteRootKeyPrefix${card.id}'),
|
||||||
padding: const EdgeInsets.all(6),
|
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(
|
child: Icon(
|
||||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue