tests
This commit is contained in:
parent
34eab39aa3
commit
dfcb7a1030
10 changed files with 107 additions and 104 deletions
|
|
@ -258,11 +258,11 @@ class UsersApiV2 {
|
|||
}
|
||||
|
||||
// Convert UserData to UserDataDto
|
||||
final userDataModel = await _userRepository.getUserById(
|
||||
final userModel = await _userRepository.getUserById(
|
||||
user.id!,
|
||||
withData: true,
|
||||
);
|
||||
if (userDataModel?.userData == null) {
|
||||
if (userModel?.userData == null) {
|
||||
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
||||
}
|
||||
|
||||
|
|
@ -297,12 +297,7 @@ class UsersApiV2 {
|
|||
|
||||
try {
|
||||
final packId = request.url.queryParameters['packId'];
|
||||
final userData = user.userData;
|
||||
|
||||
if (userData == null) {
|
||||
return _json([]);
|
||||
}
|
||||
|
||||
|
||||
// Рассчитать packProgress на лету из WordStatistics
|
||||
final allPackProgress = await _statisticsCalculator
|
||||
.calculateAllPackProgress(user.id!);
|
||||
|
|
@ -562,7 +557,7 @@ class UsersApiV2 {
|
|||
return _json([]);
|
||||
}
|
||||
|
||||
final achievements = _statisticsCalculator.calculateAchievementProgress(
|
||||
final achievements = _statisticsCalculator.calculateAchievemegntProgress(
|
||||
userData,
|
||||
);
|
||||
return _json(achievements.map((a) => a.toJson()).toList());
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/card_pack_model.dart';
|
||||
import 'package:mnemo_cards_common_backend/src/models/user/user_data_model.dart';
|
||||
|
||||
import '../subscription/user_subscription_model.dart';
|
||||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
|
|
@ -18,9 +15,7 @@ class UserModel {
|
|||
@JsonKey(defaultValue: [])
|
||||
final List<String> purchases;
|
||||
final List<CardPackModel> packs;
|
||||
UserDataModel? userData;
|
||||
UserSubscriptionModel? subscriptionModel;
|
||||
|
||||
|
||||
// UserSettingsDto
|
||||
final String? userSettings;
|
||||
|
||||
|
|
|
|||
|
|
@ -231,37 +231,14 @@ class GameSessionManager {
|
|||
if (answer is! String) return false;
|
||||
|
||||
// Remove spaces from answer
|
||||
final answerNoSpaces = answer.replaceAll(' ', '');
|
||||
final correctNoSpaces = question.correctAnswer.replaceAll(' ', '');
|
||||
final answerNoSpaces = answer.replaceAll(' ', '').toLowerCase();
|
||||
final correctNoSpaces = question.correctAnswer.replaceAll(' ', '').toLowerCase();
|
||||
|
||||
// If lengths don't match, answer is wrong
|
||||
if (answerNoSpaces.length != correctNoSpaces.length) return false;
|
||||
|
||||
// Build expected answer based on template
|
||||
// '_' = lowercase, '|' = uppercase
|
||||
final template = question.template;
|
||||
final expectedAnswer = StringBuffer();
|
||||
int answerLetterIndex = 0;
|
||||
|
||||
for (int i = 0; i < template.length && answerLetterIndex < correctNoSpaces.length; i++) {
|
||||
final templateChar = template[i];
|
||||
if (templateChar == '_' || templateChar == '|') {
|
||||
// This is a slot - use the corresponding letter from correct answer
|
||||
final correctChar = correctNoSpaces[answerLetterIndex];
|
||||
if (templateChar == '|') {
|
||||
// Uppercase slot - expect uppercase
|
||||
expectedAnswer.write(correctChar.toUpperCase());
|
||||
} else {
|
||||
// Lowercase slot - expect lowercase
|
||||
expectedAnswer.write(correctChar.toLowerCase());
|
||||
}
|
||||
answerLetterIndex++;
|
||||
}
|
||||
// Skip other characters in template (visible letters, spaces)
|
||||
}
|
||||
|
||||
|
||||
// Compare user answer with expected answer (case-sensitive for uppercase slots)
|
||||
return answerNoSpaces == expectedAnswer.toString();
|
||||
return answerNoSpaces == question.correctAnswer;
|
||||
}
|
||||
|
||||
bool _validateMatch(MatchQuestion question, dynamic answer) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:game_tests/game_tests.dart';
|
||||
|
|
@ -521,6 +522,20 @@ class TestsStateManager extends StateManager<TestsState> {
|
|||
} else if (question is MatrixTestQuestionBody) {
|
||||
final id = question.id?.toString() ?? 'q_${questions.length}';
|
||||
|
||||
// Validate and log matrix question data
|
||||
if (question.cards.isEmpty) {
|
||||
debugPrint(
|
||||
'TestsStateManager: Warning - matrix question $id has empty cards list. '
|
||||
'matrixSize: ${question.matrixSize}, stages: ${question.stages.length}',
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'TestsStateManager: Converting matrix question $id with '
|
||||
'${question.cards.length} cards, matrixSize: ${question.matrixSize}, '
|
||||
'stages: ${question.stages.length}',
|
||||
);
|
||||
}
|
||||
|
||||
final cards = question.cards
|
||||
.map(
|
||||
(c) => MatrixCard(
|
||||
|
|
|
|||
|
|
@ -906,7 +906,6 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
|||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.cover,
|
||||
useCachedNetworkImage: true,
|
||||
maxWidthDiskCache: 2048,
|
||||
maxHeightDiskCache: 2048,
|
||||
fadeInDuration: const Duration(milliseconds: 100),
|
||||
|
|
|
|||
|
|
@ -724,7 +724,6 @@ class _CardSide extends StatelessWidget {
|
|||
card: card,
|
||||
packId: packId,
|
||||
fit: BoxFit.cover,
|
||||
useCachedNetworkImage: false,
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
color: packColor.withOpacity(0.1),
|
||||
|
|
|
|||
|
|
@ -553,7 +553,6 @@ class _CardSide extends StatelessWidget {
|
|||
card: card,
|
||||
packId: packId,
|
||||
fit: BoxFit.cover,
|
||||
useCachedNetworkImage: false,
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
color: packColor.withOpacity(0.1),
|
||||
|
|
@ -575,7 +574,6 @@ class _CardSide extends StatelessWidget {
|
|||
packId: packId,
|
||||
fit: BoxFit.contain,
|
||||
isBackImage: true,
|
||||
useCachedNetworkImage: false,
|
||||
placeholder: (context, url) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class ExpiringCardImage extends StatefulWidget {
|
|||
this.fadeInDuration = const Duration(milliseconds: 100),
|
||||
this.fadeOutDuration = const Duration(milliseconds: 100),
|
||||
this.isBackImage = false,
|
||||
this.useCachedNetworkImage = true,
|
||||
});
|
||||
|
||||
final GameCardDto card;
|
||||
|
|
@ -48,7 +47,6 @@ class ExpiringCardImage extends StatefulWidget {
|
|||
final Duration fadeInDuration;
|
||||
final Duration fadeOutDuration;
|
||||
final bool isBackImage;
|
||||
final bool useCachedNetworkImage;
|
||||
|
||||
@override
|
||||
State<ExpiringCardImage> createState() => _ExpiringCardImageState();
|
||||
|
|
@ -191,22 +189,6 @@ class _ExpiringCardImageState extends State<ExpiringCardImage> {
|
|||
);
|
||||
|
||||
if (mounted) {
|
||||
// Clear cache for old URL before updating
|
||||
if (_currentUrl != null && widget.useCachedNetworkImage) {
|
||||
try {
|
||||
await CachedNetworkImage.evictFromCache(_currentUrl!);
|
||||
log(
|
||||
'Cleared cache for old URL: $_currentUrl',
|
||||
name: 'ExpiringCardImage',
|
||||
);
|
||||
} catch (e) {
|
||||
log(
|
||||
'Failed to clear cache: $e',
|
||||
name: 'ExpiringCardImage',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentUrl = newUrl;
|
||||
_isRefreshing = false;
|
||||
|
|
@ -360,38 +342,26 @@ class _ExpiringCardImageState extends State<ExpiringCardImage> {
|
|||
return _buildErrorWidget(context, '', null);
|
||||
}
|
||||
|
||||
if (widget.useCachedNetworkImage) {
|
||||
// Use key based on URL to force rebuild when URL changes
|
||||
return CachedNetworkImage(
|
||||
key: ValueKey('cached_image_$_currentUrl'),
|
||||
imageUrl: _currentUrl!,
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
maxWidthDiskCache: widget.maxWidthDiskCache,
|
||||
maxHeightDiskCache: widget.maxHeightDiskCache,
|
||||
fadeInDuration: widget.fadeInDuration,
|
||||
fadeOutDuration: widget.fadeOutDuration,
|
||||
placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(),
|
||||
errorWidget: _buildErrorWidget,
|
||||
// Clear cache on error to allow retry with new URL
|
||||
cacheKey: _currentUrl,
|
||||
);
|
||||
} else {
|
||||
return Image.network(
|
||||
_currentUrl!,
|
||||
key: ValueKey('network_image_$_currentUrl'),
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildErrorWidget(context, _currentUrl!, error);
|
||||
},
|
||||
loadingBuilder: widget.placeholder != null
|
||||
? null
|
||||
: (context, child, loadingProgress) => child,
|
||||
);
|
||||
}
|
||||
final objectId = _getObjectId();
|
||||
// Use objectId as cacheKey so cache persists across URL refreshes
|
||||
final cacheKey = objectId ?? _currentUrl;
|
||||
|
||||
// Use key based on objectId to force rebuild when objectId changes
|
||||
return CachedNetworkImage(
|
||||
key: ValueKey('cached_image_$cacheKey'),
|
||||
imageUrl: _currentUrl!,
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
maxWidthDiskCache: widget.maxWidthDiskCache,
|
||||
maxHeightDiskCache: widget.maxHeightDiskCache,
|
||||
fadeInDuration: widget.fadeInDuration,
|
||||
fadeOutDuration: widget.fadeOutDuration,
|
||||
placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(),
|
||||
errorWidget: _buildErrorWidget,
|
||||
// Cache by objectId (UUID) instead of URL, so cache persists when presigned URL refreshes
|
||||
cacheKey: cacheKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,19 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
|||
}
|
||||
|
||||
void _initFromQuestion(MatrixQuestion q) {
|
||||
// Validate that cards are not empty
|
||||
if (q.cards.isEmpty) {
|
||||
debugPrint(
|
||||
'MatrixWidget: Warning - question ${q.id} has empty cards list. '
|
||||
'matrixSize: ${q.matrixSize}, stages: ${q.stages.length}',
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'MatrixWidget: Initializing question ${q.id} with '
|
||||
'${q.cards.length} cards, matrixSize: ${q.matrixSize}',
|
||||
);
|
||||
}
|
||||
|
||||
_slots = q.cards.map<MatrixCard?>((c) => c).toList(growable: false);
|
||||
_flipped.clear();
|
||||
_correctIdsInOrder.clear();
|
||||
|
|
@ -356,10 +369,25 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
|||
|
||||
Widget _buildSlot(BuildContext context, MatrixCard? card) {
|
||||
if (card == null) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r)),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(24.r),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
size: 32.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -637,10 +665,10 @@ class _MatrixFlipCard extends StatelessWidget {
|
|||
}
|
||||
|
||||
// If both image and text, show text on top and image below
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (hasText)
|
||||
if (hasImage && hasText) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(2.w, 12.h, 2.w, 8.h),
|
||||
child: Column(
|
||||
|
|
@ -673,7 +701,6 @@ class _MatrixFlipCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (hasImage)
|
||||
Expanded(
|
||||
child: Image.network(
|
||||
card.image!,
|
||||
|
|
@ -689,7 +716,36 @@ class _MatrixFlipCard extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: if no image and no text, show card ID as placeholder
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.w),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.help_outline,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
size: 48.sp,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
card.id,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,7 +255,6 @@ class PackCardItem extends StatelessWidget {
|
|||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
fit: BoxFit.contain,
|
||||
useCachedNetworkImage: false,
|
||||
placeholder: (context, url) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
|
|
|
|||
Loading…
Reference in a new issue