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
|
// Convert UserData to UserDataDto
|
||||||
final userDataModel = await _userRepository.getUserById(
|
final userModel = await _userRepository.getUserById(
|
||||||
user.id!,
|
user.id!,
|
||||||
withData: true,
|
withData: true,
|
||||||
);
|
);
|
||||||
if (userDataModel?.userData == null) {
|
if (userModel?.userData == null) {
|
||||||
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,12 +297,7 @@ class UsersApiV2 {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final packId = request.url.queryParameters['packId'];
|
final packId = request.url.queryParameters['packId'];
|
||||||
final userData = user.userData;
|
|
||||||
|
|
||||||
if (userData == null) {
|
|
||||||
return _json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рассчитать packProgress на лету из WordStatistics
|
// Рассчитать packProgress на лету из WordStatistics
|
||||||
final allPackProgress = await _statisticsCalculator
|
final allPackProgress = await _statisticsCalculator
|
||||||
.calculateAllPackProgress(user.id!);
|
.calculateAllPackProgress(user.id!);
|
||||||
|
|
@ -562,7 +557,7 @@ class UsersApiV2 {
|
||||||
return _json([]);
|
return _json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final achievements = _statisticsCalculator.calculateAchievementProgress(
|
final achievements = _statisticsCalculator.calculateAchievemegntProgress(
|
||||||
userData,
|
userData,
|
||||||
);
|
);
|
||||||
return _json(achievements.map((a) => a.toJson()).toList());
|
return _json(achievements.map((a) => a.toJson()).toList());
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:copy_with_extension/copy_with_extension.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/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';
|
part 'user_model.g.dart';
|
||||||
|
|
||||||
|
|
@ -18,9 +15,7 @@ class UserModel {
|
||||||
@JsonKey(defaultValue: [])
|
@JsonKey(defaultValue: [])
|
||||||
final List<String> purchases;
|
final List<String> purchases;
|
||||||
final List<CardPackModel> packs;
|
final List<CardPackModel> packs;
|
||||||
UserDataModel? userData;
|
|
||||||
UserSubscriptionModel? subscriptionModel;
|
|
||||||
|
|
||||||
// UserSettingsDto
|
// UserSettingsDto
|
||||||
final String? userSettings;
|
final String? userSettings;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,37 +231,14 @@ class GameSessionManager {
|
||||||
if (answer is! String) return false;
|
if (answer is! String) return false;
|
||||||
|
|
||||||
// Remove spaces from answer
|
// Remove spaces from answer
|
||||||
final answerNoSpaces = answer.replaceAll(' ', '');
|
final answerNoSpaces = answer.replaceAll(' ', '').toLowerCase();
|
||||||
final correctNoSpaces = question.correctAnswer.replaceAll(' ', '');
|
final correctNoSpaces = question.correctAnswer.replaceAll(' ', '').toLowerCase();
|
||||||
|
|
||||||
// If lengths don't match, answer is wrong
|
// If lengths don't match, answer is wrong
|
||||||
if (answerNoSpaces.length != correctNoSpaces.length) return false;
|
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)
|
// 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) {
|
bool _validateMatch(MatchQuestion question, dynamic answer) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:game_tests/game_tests.dart';
|
import 'package:game_tests/game_tests.dart';
|
||||||
|
|
@ -521,6 +522,20 @@ class TestsStateManager extends StateManager<TestsState> {
|
||||||
} else if (question is MatrixTestQuestionBody) {
|
} else if (question is MatrixTestQuestionBody) {
|
||||||
final id = question.id?.toString() ?? 'q_${questions.length}';
|
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
|
final cards = question.cards
|
||||||
.map(
|
.map(
|
||||||
(c) => MatrixCard(
|
(c) => MatrixCard(
|
||||||
|
|
|
||||||
|
|
@ -906,7 +906,6 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
useCachedNetworkImage: true,
|
|
||||||
maxWidthDiskCache: 2048,
|
maxWidthDiskCache: 2048,
|
||||||
maxHeightDiskCache: 2048,
|
maxHeightDiskCache: 2048,
|
||||||
fadeInDuration: const Duration(milliseconds: 100),
|
fadeInDuration: const Duration(milliseconds: 100),
|
||||||
|
|
|
||||||
|
|
@ -724,7 +724,6 @@ class _CardSide extends StatelessWidget {
|
||||||
card: card,
|
card: card,
|
||||||
packId: packId,
|
packId: packId,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
useCachedNetworkImage: false,
|
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
return Container(
|
return Container(
|
||||||
color: packColor.withOpacity(0.1),
|
color: packColor.withOpacity(0.1),
|
||||||
|
|
|
||||||
|
|
@ -553,7 +553,6 @@ class _CardSide extends StatelessWidget {
|
||||||
card: card,
|
card: card,
|
||||||
packId: packId,
|
packId: packId,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
useCachedNetworkImage: false,
|
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
return Container(
|
return Container(
|
||||||
color: packColor.withOpacity(0.1),
|
color: packColor.withOpacity(0.1),
|
||||||
|
|
@ -575,7 +574,6 @@ class _CardSide extends StatelessWidget {
|
||||||
packId: packId,
|
packId: packId,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
isBackImage: true,
|
isBackImage: true,
|
||||||
useCachedNetworkImage: false,
|
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ class ExpiringCardImage extends StatefulWidget {
|
||||||
this.fadeInDuration = const Duration(milliseconds: 100),
|
this.fadeInDuration = const Duration(milliseconds: 100),
|
||||||
this.fadeOutDuration = const Duration(milliseconds: 100),
|
this.fadeOutDuration = const Duration(milliseconds: 100),
|
||||||
this.isBackImage = false,
|
this.isBackImage = false,
|
||||||
this.useCachedNetworkImage = true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameCardDto card;
|
final GameCardDto card;
|
||||||
|
|
@ -48,7 +47,6 @@ class ExpiringCardImage extends StatefulWidget {
|
||||||
final Duration fadeInDuration;
|
final Duration fadeInDuration;
|
||||||
final Duration fadeOutDuration;
|
final Duration fadeOutDuration;
|
||||||
final bool isBackImage;
|
final bool isBackImage;
|
||||||
final bool useCachedNetworkImage;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ExpiringCardImage> createState() => _ExpiringCardImageState();
|
State<ExpiringCardImage> createState() => _ExpiringCardImageState();
|
||||||
|
|
@ -191,22 +189,6 @@ class _ExpiringCardImageState extends State<ExpiringCardImage> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
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(() {
|
setState(() {
|
||||||
_currentUrl = newUrl;
|
_currentUrl = newUrl;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
|
|
@ -360,38 +342,26 @@ class _ExpiringCardImageState extends State<ExpiringCardImage> {
|
||||||
return _buildErrorWidget(context, '', null);
|
return _buildErrorWidget(context, '', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.useCachedNetworkImage) {
|
final objectId = _getObjectId();
|
||||||
// Use key based on URL to force rebuild when URL changes
|
// Use objectId as cacheKey so cache persists across URL refreshes
|
||||||
return CachedNetworkImage(
|
final cacheKey = objectId ?? _currentUrl;
|
||||||
key: ValueKey('cached_image_$_currentUrl'),
|
|
||||||
imageUrl: _currentUrl!,
|
// Use key based on objectId to force rebuild when objectId changes
|
||||||
fit: widget.fit,
|
return CachedNetworkImage(
|
||||||
width: widget.width,
|
key: ValueKey('cached_image_$cacheKey'),
|
||||||
height: widget.height,
|
imageUrl: _currentUrl!,
|
||||||
maxWidthDiskCache: widget.maxWidthDiskCache,
|
fit: widget.fit,
|
||||||
maxHeightDiskCache: widget.maxHeightDiskCache,
|
width: widget.width,
|
||||||
fadeInDuration: widget.fadeInDuration,
|
height: widget.height,
|
||||||
fadeOutDuration: widget.fadeOutDuration,
|
maxWidthDiskCache: widget.maxWidthDiskCache,
|
||||||
placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(),
|
maxHeightDiskCache: widget.maxHeightDiskCache,
|
||||||
errorWidget: _buildErrorWidget,
|
fadeInDuration: widget.fadeInDuration,
|
||||||
// Clear cache on error to allow retry with new URL
|
fadeOutDuration: widget.fadeOutDuration,
|
||||||
cacheKey: _currentUrl,
|
placeholder: widget.placeholder ?? (context, url) => const SizedBox.shrink(),
|
||||||
);
|
errorWidget: _buildErrorWidget,
|
||||||
} else {
|
// Cache by objectId (UUID) instead of URL, so cache persists when presigned URL refreshes
|
||||||
return Image.network(
|
cacheKey: cacheKey,
|
||||||
_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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,19 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initFromQuestion(MatrixQuestion q) {
|
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);
|
_slots = q.cards.map<MatrixCard?>((c) => c).toList(growable: false);
|
||||||
_flipped.clear();
|
_flipped.clear();
|
||||||
_correctIdsInOrder.clear();
|
_correctIdsInOrder.clear();
|
||||||
|
|
@ -356,10 +369,25 @@ class _MatrixWidgetState extends State<MatrixWidget>
|
||||||
|
|
||||||
Widget _buildSlot(BuildContext context, MatrixCard? card) {
|
Widget _buildSlot(BuildContext context, MatrixCard? card) {
|
||||||
if (card == null) {
|
if (card == null) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: DecoratedBox(
|
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
|
// If both image and text, show text on top and image below
|
||||||
return Column(
|
if (hasImage && hasText) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
return Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
if (hasText)
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(2.w, 12.h, 2.w, 8.h),
|
padding: EdgeInsets.fromLTRB(2.w, 12.h, 2.w, 8.h),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -673,7 +701,6 @@ class _MatrixFlipCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasImage)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
card.image!,
|
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,
|
width: cardWidth,
|
||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
useCachedNetworkImage: false,
|
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
return Center(
|
return Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue