207 lines
6 KiB
Dart
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);
|
|
}
|
|
}
|