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 _triggerController = StreamController.broadcast(); TestDto? _test; Color get color => _test?.color?.asColor ?? borderGray; List get questions => _test?.questions ?? []; void triggerPoll() => _triggerController.add(1); Future> 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> 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([])), ) .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 get states => testHolder.state.questions.values; Future 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 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 loadImage(String packId, String path) async { final bytes = await packCacheManager.loadPackFile(packId, path); return MemoryImage(bytes!); } Future sendStatistics() { final words = states .fold({}, (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); } }