mnemo_cards/lib/features/tests/test_manager.dart
2024-06-13 08:06:50 +03:00

207 lines
6 KiB
Dart

import 'dart:async';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:mnemo_cards/features/tests/test_state_holder.dart';
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
import 'package:mnemo_cards/features/packs/pack_cache_manager.dart';
import 'package:mnemo_cards/main.dart';
import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards/utils/string_helper.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:rxdart/rxdart.dart';
import '../../managers/repository/http_repository.dart';
import 'question_states/test_question_state.dart';
class TestManager extends ChangeNotifier {
final PackCacheManager packCacheManager;
final HttpRepository _repository;
final PageController pageController = PageController();
final TestHolder testHolder = TestHolder();
final StreamController<void> _triggerController =
StreamController.broadcast();
TestDto? _test;
Color get color => _test?.color?.asColor ?? borderGray;
List<AbstractTestQuestion> get questions => _test?.questions ?? [];
void triggerPoll() => _triggerController.add(1);
Future<List<TestDto>> pollTests({String? packId}) async {
print('polling tests');
final tests = (await _repository.getTests(packId: packId ?? ''));
return tests;
}
void skip(int id) {
testHolder.skip(id);
notifyListeners();
}
Stream<List<TestDto>> loadTestsStream({String? packId}) {
return Rx.combineLatest2(
_triggerController.stream.startWith(null),
Stream.periodic(
Duration(seconds: 120),
).startWith(null),
(_, __) => null,
)
.throttleTime(Duration(seconds: 1))
.asyncMap(
(tests) async => pollTests(packId: packId)
.catchError((_) => Future.value(<TestDto>[])),
)
.where((test) => test.isNotEmpty)
.distinct((p, n) {
if (p.length == n.length) {
for (int i = 0; i < p.length; i++) {
if (p[i].id != n[i].id ||
p[i].version != n[i].version ||
!ListEquality().equals(
p[i].statistics?.words.words,
n[i].statistics?.words.words,
)) {
return false;
}
}
}
return false;
});
}
TestQuestionState? getState(int id) => testHolder.testQuestionState(id);
Iterable<TestQuestionState> get states => testHolder.state.questions.values;
Future<void> setActiveTest(String id, {String? packId}) async {
clearStates();
if (packId != null) {
await packCacheManager.loadPackFromCache(packId);
}
try {
_test = await _repository.getTest(id);
} on Object catch (e, s) {
log('error when loading test ${id}', error: e, stackTrace: s);
}
if (_test == null) {
appRouter.maybePop();
return;
}
// todo use uuid
testHolder.setQuestions(
_test!.questions,
DateTime.timestamp().toString(),
);
}
void setState(int id, TestQuestionState state) {
testHolder.setQuestionState(id, state);
log('Set state $id $state');
notifyListeners();
}
TestManager(
this.packCacheManager,
this._repository,
);
void clearStates() {
testHolder.clear();
notifyListeners();
}
AbstractTestQuestion getTest(int index) => questions[index];
void nextTest() {
pageController.animateToPage(
pageController.page!.toInt() + 1,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
void prevTest() {
pageController.animateToPage(
pageController.page!.toInt() - 1,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
Future<void> preloadImages(BuildContext context) async {
for (final question in questions) {
switch (question.questionType) {
case TestQuestionType.simple:
final images = [
(question as SimpleTestQuestionBody).image,
...question.buttons.map((e) => e.image)
].whereNotNull();
for (final image in images) {
try {
final testImage = TestImage.image(image);
await precacheImage(
(await testImage.loadImage())!,
context,
);
} catch (e) {
rethrow;
}
}
case TestQuestionType.input_buttons:
final images = [
(question as InputButtonsTestQuestionBody).image,
].whereNotNull();
for (final image in images) {
try {
final testImage = TestImage.image(image);
await precacheImage(
(await testImage.loadImage())!,
context,
);
} catch (e) {
rethrow;
}
}
case TestQuestionType.matrix:
// TODO: Handle this case.
case TestQuestionType.match:
// TODO: Handle this case.
case TestQuestionType.undefined:
// TODO: Handle this case.
}
}
}
Future<MemoryImage> loadImage(String packId, String path) async {
final bytes = await packCacheManager.loadPackFile(packId, path);
return MemoryImage(bytes!);
}
Future<void> sendStatistics() {
final words = states
.fold(<String, WordStatisticsDto>{}, (stats, s) {
final existStat = stats[s.word] ?? WordStatisticsDto.empty(s.word);
stats[s.word] = existStat.copyWith(
correct: existStat.correct + (s.isCorrect ? 1 : 0),
incorrect:
existStat.incorrect + ((!s.isCorrect && s.isAnswered) ? 1 : 0),
skipped: existStat.skipped + (s.isAnswered ? 0 : 1),
questionTypes: {...existStat.questionTypes, s.testType},
);
return stats;
})
.values
.toList();
final dto = TestStatisticsDto(
testId: int.tryParse(_test?.id ?? '') ?? -1,
words: AllWordsStatisticsDto(words: words),
sessionToken: testHolder.state.sessionToken,
);
return _repository.addTestStatistics(dto);
}
}