tests and stats
This commit is contained in:
parent
a6d5583c3f
commit
69930bd9df
22 changed files with 425 additions and 547 deletions
Binary file not shown.
|
|
@ -14,6 +14,7 @@ import 'package:mnemo_cards/widgets/header.dart';
|
|||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
|
||||
import '../main.dart';
|
||||
import '../widgets/image_fade.dart';
|
||||
|
||||
|
|
@ -256,7 +257,10 @@ class AddPackage with Api {
|
|||
'Preview cards',
|
||||
cardPackDto.previewCards?.toString(),
|
||||
(v) {
|
||||
final cards = (jsonDecode(v) as List).cast<String>();
|
||||
final cards = v
|
||||
.decodeList<int>()
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
cardPackDto = cardPackDto.copyWith.previewCards(cards);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
|
@ -8,6 +7,7 @@ import 'package:mnemo_cards/features/packs/packs_api.dart';
|
|||
import 'package:mnemo_cards/managers/repository/api.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
|
||||
class AdminApi with Api {
|
||||
final Dio _dio;
|
||||
|
||||
|
|
@ -41,10 +41,7 @@ class AdminApi with Api {
|
|||
'$path/cards',
|
||||
queryParameters: {'ids': ids.join(',')},
|
||||
);
|
||||
return (jsonDecode(r.data!) as List)
|
||||
.map((e) => GameCardDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
.cast();
|
||||
return (r.data as String).decodeList(GameCardDto.fromJson);
|
||||
}
|
||||
|
||||
Future<List<UserDto>> getUsers(List<String> ids) async {
|
||||
|
|
@ -94,9 +91,7 @@ class AdminApi with Api {
|
|||
final r = await _dio.get<String>(
|
||||
'$path/pack/edit/$id',
|
||||
);
|
||||
return EditCardPackDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
);
|
||||
return (r.data as String).decode(EditCardPackDto.fromJson);
|
||||
}
|
||||
|
||||
Future<bool> deletePack(EditCardPackDto dto) async {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
|
@ -27,11 +28,12 @@ class PackCacheManager {
|
|||
final dir = await _packDirectory(packId);
|
||||
final file = File('${dir.path}/pack.json');
|
||||
if (file.existsSync()) {
|
||||
final json = jsonEncode(await file.readAsString()) as Map;
|
||||
final json = (await file.readAsString()).decode<Map>();
|
||||
json['id'] = '0';
|
||||
await file.writeAsString(jsonEncode(json));
|
||||
}
|
||||
} catch (e, s) {
|
||||
log('Cant marck pack for update', error: e, stackTrace: s);
|
||||
Analytics.crashlyticsError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -52,9 +54,7 @@ class PackCacheManager {
|
|||
log('Pack file not exist');
|
||||
return throw Exception('Pack file not exist');
|
||||
}
|
||||
var pack = CardPackDto.fromJson(
|
||||
jsonDecode(packFile.readAsStringSync()),
|
||||
);
|
||||
var pack = packFile.readAsStringSync().decode(CardPackDto.fromJson);
|
||||
if (withCardImages) {
|
||||
Map<String, MemoryImage> images = {};
|
||||
log('Cards ${packId}: ${pack.cards.length}');
|
||||
|
|
@ -147,7 +147,7 @@ class PackCacheManager {
|
|||
}) async {
|
||||
log('Saving pack ${dto.id}');
|
||||
final dir = await _packDirectory(dto.id);
|
||||
final json = jsonEncode(dto);
|
||||
final json = dto.encode();
|
||||
log('Saving dto ${dto.id}');
|
||||
File('${dir.path}/pack.json')
|
||||
..createSync(recursive: true)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
|
@ -19,10 +18,8 @@ class PacksApi with Api {
|
|||
'$path/packs/previews',
|
||||
queryParameters: packData,
|
||||
);
|
||||
final previews = (jsonDecode(r.data!) as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(CardPackPreviewDto.fromJson)
|
||||
.toList();
|
||||
final previews =
|
||||
(r.data! as String).decodeList(CardPackPreviewDto.fromJson).toList();
|
||||
return previews;
|
||||
}
|
||||
|
||||
|
|
@ -34,10 +31,7 @@ class PacksApi with Api {
|
|||
'$path/packs/actions',
|
||||
queryParameters: packData,
|
||||
);
|
||||
final actions = (jsonDecode(r.data!) as List<dynamic>)
|
||||
.cast<Map<String, Object?>>()
|
||||
.map(CardPackAction.fromJson)
|
||||
.toList();
|
||||
final actions = (r.data! as String).decodeList(CardPackAction.fromJson);
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
|
@ -50,9 +44,7 @@ class PacksApi with Api {
|
|||
receiveTimeout: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
return CardPackDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
);
|
||||
return (r.data as String).decode(CardPackDto.fromJson);
|
||||
}
|
||||
|
||||
// buy page dto if not available
|
||||
|
|
@ -64,9 +56,7 @@ class PacksApi with Api {
|
|||
receiveTimeout: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
return CardPackBuyDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
);
|
||||
return (r.data as String).decode(CardPackBuyDto.fromJson);
|
||||
}
|
||||
|
||||
/// cards zip archive for available pack
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
|
@ -30,9 +31,7 @@ class PreviewPackHolder {
|
|||
final List<CardPackPreviewDto> previewPacks = [];
|
||||
previewPacksStrings.forEach((string) {
|
||||
try {
|
||||
final pack = CardPackPreviewDto.fromJson(
|
||||
jsonDecode(string) as Map<String, Object?>,
|
||||
);
|
||||
final pack = string.decode(CardPackPreviewDto.fromJson);
|
||||
previewPacks.add(pack);
|
||||
} catch (e, s) {
|
||||
log('Cant load preview pack', error: e, stackTrace: s);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import 'dart:math';
|
|||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/flags.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../theme/themes.dart';
|
||||
|
|
@ -122,6 +124,10 @@ class _ProgressWidgetState extends State<ProgressWidget> {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (kDebugMode)
|
||||
Text(
|
||||
'${(results.where((r) => r != Result.not_visited).length)}/${results.length}',
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ class InputButtonsTestState extends TestQuestionState {
|
|||
this.isCorrect = false,
|
||||
this.isAnswered = false,
|
||||
this.isSkipped = false,
|
||||
super.testType = TestQuestionType.simple,
|
||||
super.testType = TestQuestionType.input_buttons,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||
import 'package:mnemo_cards/utils/color_helper.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../di/locator.dart';
|
||||
import '../../widgets/big_back_button.dart';
|
||||
import '../../widgets/horizontal_progress.dart';
|
||||
import 'question_states/test_question_state.dart';
|
||||
|
|
@ -37,8 +38,18 @@ class TestCompleteWidget extends StatelessWidget {
|
|||
)
|
||||
.values
|
||||
.toList()
|
||||
..sort((p, n) => p.correctCount.compareTo(n.correctCount));
|
||||
return Column(
|
||||
..sort(
|
||||
(p, n) => (p.correctCount.toDouble() / p.totalCount)
|
||||
.compareTo(n.correctCount.toDouble() / n.totalCount),
|
||||
);
|
||||
return PopScope(
|
||||
onPopInvoked: (v) {
|
||||
if (v) {
|
||||
locator.testManager.sendStatistics();
|
||||
locator.testManager.triggerPoll();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
|
|
@ -163,6 +174,7 @@ class TestCompleteWidget extends StatelessWidget {
|
|||
),
|
||||
const BigBackButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:mnemo_cards/features/packs/preview_packs_poller.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';
|
||||
|
|
@ -18,15 +19,22 @@ import 'question_states/test_question_state.dart';
|
|||
class TestManager extends ChangeNotifier {
|
||||
final PackCacheManager packCacheManager;
|
||||
final HttpRepository _repository;
|
||||
final List<AbstractTestQuestion> questions = [];
|
||||
Color color = borderGray;
|
||||
final PageController pageController = PageController();
|
||||
final TestHolder testHolder = TestHolder();
|
||||
final StreamController<void> _triggerController =
|
||||
StreamController.broadcast();
|
||||
|
||||
Future<List<TestDto>> loadTests({String? packId}) async {
|
||||
final tests = (await _repository.getTests(packId: packId ?? ''))
|
||||
.map((e) => TestDto.fromJson(e))
|
||||
.toList();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -36,12 +44,16 @@ class TestManager extends ChangeNotifier {
|
|||
}
|
||||
|
||||
Stream<List<TestDto>> loadTestsStream({String? packId}) {
|
||||
return Stream.periodic(
|
||||
Duration(seconds: 60),
|
||||
return Rx.combineLatest2(
|
||||
_triggerController.stream.startWith(null),
|
||||
Stream.periodic(
|
||||
Duration(seconds: 120),
|
||||
).startWith(null),
|
||||
(_, __) => null,
|
||||
)
|
||||
.startWith(null)
|
||||
.throttleTime(Duration(seconds: 1))
|
||||
.asyncMap(
|
||||
(tests) async => loadTests(packId: packId)
|
||||
(tests) async => pollTests(packId: packId)
|
||||
.catchError((_) => Future.value(<TestDto>[])),
|
||||
)
|
||||
.where((test) => test.isNotEmpty);
|
||||
|
|
@ -56,19 +68,20 @@ class TestManager extends ChangeNotifier {
|
|||
if (packId != null) {
|
||||
await packCacheManager.loadPackFromCache(packId);
|
||||
}
|
||||
TestDto? test;
|
||||
try {
|
||||
test = await _repository.getTest(id);
|
||||
_test = await _repository.getTest(id);
|
||||
} on Object catch (e, s) {
|
||||
log('error when loading test ${id}');
|
||||
log('error when loading test ${id}', error: e, stackTrace: s);
|
||||
}
|
||||
if (test == null) {
|
||||
if (_test == null) {
|
||||
appRouter.maybePop();
|
||||
return;
|
||||
}
|
||||
questions.clear();
|
||||
questions.addAll(test!.questions);
|
||||
color = test.color?.asColor ?? borderGray;
|
||||
testHolder.setQuestions(questions);
|
||||
// todo use uuid
|
||||
testHolder.setQuestions(
|
||||
_test!.questions,
|
||||
DateTime.timestamp().toString(),
|
||||
);
|
||||
}
|
||||
|
||||
void setState(int id, TestQuestionState state) {
|
||||
|
|
@ -153,4 +166,28 @@ class TestManager extends ChangeNotifier {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import 'dart:developer';
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/managers/audio_player.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../di/locator.dart';
|
||||
import 'scroll_physics.dart';
|
||||
|
|
@ -26,6 +29,9 @@ class TestPage extends StatefulWidget {
|
|||
class _TestPageState extends State<TestPage> {
|
||||
PageController get pageController => locator.testManager.pageController;
|
||||
|
||||
int? lastSkipIndex;
|
||||
int? _lastAudioQuestionId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -41,25 +47,46 @@ class _TestPageState extends State<TestPage> {
|
|||
}
|
||||
|
||||
void _pageScrollListener() {
|
||||
final index = pageController.page?.floor() ?? 0;
|
||||
if (!pageController.hasClients) {
|
||||
return;
|
||||
}
|
||||
final index = pageController.page!.floor();
|
||||
print('Current index $index, lastAudio $_lastAudioQuestionId');
|
||||
|
||||
if (index < locator.testManager.questions.length) {
|
||||
final question = locator.testManager.questions[index];
|
||||
if (question.id != _lastAudioQuestionId) {
|
||||
print('AUDIO $_lastAudioQuestionId ${question.id}');
|
||||
_lastAudioQuestionId = question.id;
|
||||
try {
|
||||
AudioPlayer.playAudio((question as dynamic).audio);
|
||||
} catch (e, s) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (index == lastSkipIndex) {
|
||||
return;
|
||||
}
|
||||
print('Will try to skip $index, ${locator.testManager.questions.length}');
|
||||
if (index > 0 && index <= locator.testManager.questions.length) {
|
||||
final prevId = locator.testManager.questions[index - 1].id!;
|
||||
final state = locator.testManager.getState(prevId);
|
||||
if (state?.isAnswered == false) {
|
||||
// log('Question skipped');
|
||||
print('Skipping $index ${state?.word}');
|
||||
locator.testManager.skip(prevId);
|
||||
lastSkipIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int lastTestIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: StreamBuilder(
|
||||
stream: locator.testManager.testHolder.asStream,
|
||||
stream: locator.testManager.testHolder.asStream.takeWhileInclusive(
|
||||
(state) => state?.questions.isNotEmpty != true,
|
||||
),
|
||||
builder: (context, s) {
|
||||
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
||||
return Scaffold(
|
||||
|
|
@ -69,6 +96,16 @@ class _TestPageState extends State<TestPage> {
|
|||
);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_pageScrollListener();
|
||||
});
|
||||
|
||||
final keys = List.generate(
|
||||
locator.testManager.questions.length,
|
||||
(v) =>
|
||||
GlobalKey(debugLabel: locator.testManager.questions[v].word),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: locator.testManager.color.withOpacity(0.25),
|
||||
body: SafeArea(
|
||||
|
|
@ -100,27 +137,55 @@ class _TestPageState extends State<TestPage> {
|
|||
locator.testManager.color,
|
||||
);
|
||||
}
|
||||
|
||||
lastTestIndex = math.max(index, lastTestIndex);
|
||||
return switch (locator.testManager
|
||||
.getTest(index)
|
||||
.questionType) {
|
||||
final question = locator.testManager.getTest(index);
|
||||
return switch (question.questionType) {
|
||||
TestQuestionType.simple => SimpleTestWidget(
|
||||
locator.testManager.getTest(index)
|
||||
as SimpleTestQuestionBody,
|
||||
question as SimpleTestQuestionBody,
|
||||
key: keys[index],
|
||||
),
|
||||
TestQuestionType.input_buttons =>
|
||||
InputButtonsTestWidget(
|
||||
locator.testManager.getTest(index)
|
||||
as InputButtonsTestQuestionBody,
|
||||
),
|
||||
question as InputButtonsTestQuestionBody,
|
||||
key: keys[index]),
|
||||
TestQuestionType.matrix => Container(),
|
||||
TestQuestionType.match => Container(),
|
||||
// TODO: Handle this case.
|
||||
TestQuestionType.undefined =>
|
||||
throw UnimplementedError(),
|
||||
};
|
||||
}.let(
|
||||
(builder) => (c, i) =>
|
||||
builder.call(c, i)?.let((w) => kDebugMode
|
||||
? Stack(
|
||||
children: [
|
||||
w,
|
||||
Row(
|
||||
children: [
|
||||
Text('$i'),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
locator.testManager
|
||||
.pageController
|
||||
.animateToPage(
|
||||
locator
|
||||
.testManager
|
||||
.pageController
|
||||
.page!
|
||||
.toInt() +
|
||||
1,
|
||||
duration: Duration(
|
||||
milliseconds: 500),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.next_plan_outlined))
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: w),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
|||
|
||||
Stream<Duration>? timer;
|
||||
|
||||
Timer? _updateTimer;
|
||||
bool _active = false;
|
||||
|
||||
void _updateDataListener() {
|
||||
if (_updateTimer == null || !_updateTimer!.isActive) {
|
||||
_updateTimer = Timer(Duration(milliseconds: 100), _updateData);
|
||||
if (!_active) {
|
||||
_active = true;
|
||||
Timer(Duration(milliseconds: 100), () => _updateData());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,16 +54,16 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
|||
results.add(Result.not_visited);
|
||||
}
|
||||
}
|
||||
print('Update data ${results.map((e) => e.name).join(' ')}');
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
_active = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// locator.testManager.removeListener(_updateData);
|
||||
// locator.testManager.addListener(_updateData);
|
||||
locator.testManager.pageController.removeListener(_updateDataListener);
|
||||
locator.testManager.pageController.addListener(_updateDataListener);
|
||||
timer = Stream.periodic(
|
||||
|
|
@ -79,7 +80,6 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
|||
length,
|
||||
locator.testManager.color,
|
||||
timer: timer,
|
||||
key: ValueKey('TEST PROGRESS'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,20 @@ class TestHolder {
|
|||
StreamController.broadcast();
|
||||
_TestState __state;
|
||||
|
||||
TestHolder() : __state = _TestState({});
|
||||
TestHolder() : __state = _TestState({}, null);
|
||||
|
||||
void skip(int id) {
|
||||
final state = testQuestionState(id);
|
||||
print('Skipping $id ${state} ${state?.testType}');
|
||||
switch (state?.testType) {
|
||||
case TestQuestionType.simple:
|
||||
state as SimpleTestState;
|
||||
print('Skipped $id ${state.word}');
|
||||
setQuestionState(id, state.copyWith(isSkipped: true));
|
||||
break;
|
||||
case TestQuestionType.input_buttons:
|
||||
state as InputButtonsTestState;
|
||||
print('Skipped $id ${state.word}');
|
||||
setQuestionState(id, state.copyWith(isSkipped: true));
|
||||
break;
|
||||
case TestQuestionType.matrix:
|
||||
|
|
@ -40,16 +43,20 @@ class TestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
void setQuestionState(int id, TestQuestionState state) => _state = _TestState(
|
||||
void setQuestionState(int id, TestQuestionState state) {
|
||||
_state = _TestState(
|
||||
Map.from(_state.questions..[id] = state),
|
||||
_state.sessionToken,
|
||||
);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_state = _TestState({});
|
||||
_state = _TestState({}, null);
|
||||
_streamController.add(_state);
|
||||
}
|
||||
|
||||
void setQuestions(List<AbstractTestQuestion> questions) {
|
||||
void setQuestions(
|
||||
List<AbstractTestQuestion> questions, String? sessionToken) {
|
||||
final stateQuestions = <int, TestQuestionState>{};
|
||||
final seed = Random().nextInt(999);
|
||||
for (final question in questions) {
|
||||
|
|
@ -72,7 +79,7 @@ class TestHolder {
|
|||
// TODO: Handle this case.
|
||||
}
|
||||
}
|
||||
_state = _TestState(stateQuestions);
|
||||
_state = _TestState(stateQuestions, sessionToken);
|
||||
}
|
||||
|
||||
TestQuestionState? testQuestionState(int id) => _state.questions[id];
|
||||
|
|
@ -91,7 +98,8 @@ class TestHolder {
|
|||
}
|
||||
|
||||
class _TestState {
|
||||
final String? sessionToken;
|
||||
final Map<int, TestQuestionState> questions;
|
||||
|
||||
_TestState(this.questions);
|
||||
_TestState(this.questions, this.sessionToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:mnemo_cards/managers/audio_player.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../../widgets/game_card_widget.dart';
|
||||
|
|
@ -11,15 +12,17 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
|||
final String? image;
|
||||
final String? audio;
|
||||
|
||||
CommonTestQuestionWidget({
|
||||
const CommonTestQuestionWidget({
|
||||
this.text,
|
||||
this.image,
|
||||
this.audio,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).textTheme;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -49,7 +52,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
|||
'icons/sound_on.png',
|
||||
width: 26,
|
||||
),
|
||||
onPressed: () => _playAudio(),
|
||||
onPressed: () => AudioPlayer.playAudio(audio),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -66,7 +69,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
|||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
onPressed: () => _playAudio(),
|
||||
onPressed: () => AudioPlayer.playAudio(audio),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -91,7 +94,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
|||
width: 26,
|
||||
height: theme.headlineLarge?.fontSize,
|
||||
),
|
||||
onPressed: () => _playAudio(),
|
||||
onPressed: () => AudioPlayer.playAudio(audio),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
@ -100,18 +103,4 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _playAudio() async {
|
||||
if (globalSharedPreferences.getBool('sound_on') != false && audio != null) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
var playText = audio!;
|
||||
if (audio!.contains('_')) {
|
||||
final lang = audio!.split('_').first;
|
||||
playText = audio!.replaceFirst('${lang}_', '');
|
||||
await flutterTts.setLanguage(lang);
|
||||
}
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
flutterTts.speak(playText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,13 @@ class SimpleTestWidget extends StatelessWidget {
|
|||
|
||||
Color get testColor => manager.color;
|
||||
|
||||
SimpleTestWidget(this._simpleTestModel, {super.key});
|
||||
const SimpleTestWidget(this._simpleTestModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final random = Random(state.seed);
|
||||
final buttons = [...model.buttons]..shuffle(random);
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
|
|
|||
19
lib/managers/audio_player.dart
Normal file
19
lib/managers/audio_player.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class AudioPlayer {
|
||||
static Future<void> playAudio(String? audio) async {
|
||||
if (globalSharedPreferences.getBool('sound_on') != false && audio != null) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
var playText = audio;
|
||||
if (audio.contains('_')) {
|
||||
final lang = audio.split('_').first;
|
||||
playText = audio.replaceFirst('${lang}_', '');
|
||||
await flutterTts.setLanguage(lang);
|
||||
}
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
flutterTts.speak(playText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +1,18 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mnemo_cards/managers/repository/repository.dart';
|
||||
import 'package:mnemo_cards/managers/user_manager.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../features/purchase/in_app_purchase.dart';
|
||||
import '../../main.dart';
|
||||
import 'api.dart';
|
||||
import 'firebase_config_repository.dart';
|
||||
|
||||
class HttpRepository extends Repository with Api {
|
||||
final Dio _dio;
|
||||
|
||||
HttpRepository(this._dio);
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> getPacks(
|
||||
Map<String, String?>? packData) async {
|
||||
final r = await _dio.get<String>(
|
||||
'$path/packs/',
|
||||
queryParameters: packData,
|
||||
);
|
||||
final ll =
|
||||
(jsonDecode(r.data!) as List<dynamic>).cast<Map<String, Object?>>();
|
||||
return ll;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDto> get user async => UserDto.empty;
|
||||
|
||||
|
|
@ -58,12 +37,7 @@ class HttpRepository extends Repository with Api {
|
|||
});
|
||||
//todo add client header encryption ??
|
||||
final authToken = r.headers[HttpHeaders.authorizationHeader]!.last;
|
||||
return (
|
||||
UserDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
),
|
||||
authToken,
|
||||
);
|
||||
return ((r.data as String).decode(UserDto.fromJson), authToken);
|
||||
} on DioException catch (e, s) {
|
||||
log('User create error', error: e, stackTrace: s);
|
||||
rethrow;
|
||||
|
|
@ -83,24 +57,35 @@ class HttpRepository extends Repository with Api {
|
|||
Future<UserDto?> getUser() async {
|
||||
final r = await _dio.get<String>('$path/user');
|
||||
if (r.statusCode == 200) {
|
||||
return UserDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
);
|
||||
return (r.data as String).decode(UserDto.fromJson);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateUserData(String id, Map<String, Object?> data) async {}
|
||||
Future<void> updateUserData(UserDataDto data) async {
|
||||
final r = await _dio.post('$path/user/data', data: data.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> getTests({String packId = ''}) async {
|
||||
Future<void> updateUserSettings(UserSettingsDto settings) async {
|
||||
final r = await _dio.post('$path/user/settings', data: settings.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addTestStatistics(TestStatisticsDto testStatistics) async {
|
||||
final r = await _dio.post(
|
||||
'$path/user/data/add/test-statistics',
|
||||
data: testStatistics.encode(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TestDto>> getTests({String packId = ''}) async {
|
||||
final r = await _dio.get<String>(
|
||||
'$path/tests/$packId',
|
||||
);
|
||||
final ll =
|
||||
(jsonDecode(r.data!) as List<dynamic>).cast<Map<String, Object?>>();
|
||||
return ll;
|
||||
return (r.data as String).decodeList(TestDto.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -109,9 +94,7 @@ class HttpRepository extends Repository with Api {
|
|||
'$path/test/$id',
|
||||
);
|
||||
try {
|
||||
return TestDto.fromJson(
|
||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
||||
);
|
||||
return (r.data as String).decode(TestDto.fromJson);
|
||||
} catch (e, s) {
|
||||
log(e.toString(), stackTrace: s);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ abstract class Repository {
|
|||
|
||||
Future<UserDto?> getUser();
|
||||
|
||||
Future<void> updateUserData(String id, Map<String, Object?> data);
|
||||
Future<void> updateUserData(UserDataDto data);
|
||||
|
||||
Future<List<Map<String, Object?>>> getTests();
|
||||
Future<void> updateUserSettings(UserSettingsDto settings);
|
||||
|
||||
Future<void> addTestStatistics(TestStatisticsDto testStatistics);
|
||||
|
||||
Future<List<TestDto>> getTests();
|
||||
|
||||
Future<TestDto?> getTest(String id);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import '../../features/yandex_ads/yandex_ads.dart';
|
|||
|
||||
import '../../di/locator.dart';
|
||||
import '../../theme/themes.dart';
|
||||
import '../horizontal_progress.dart';
|
||||
import 'card_pack_header.dart';
|
||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||
|
||||
|
|
@ -180,6 +181,8 @@ class _TestsSection extends StatelessWidget {
|
|||
child: _TestButton(
|
||||
title: dto.name,
|
||||
subtitle: null,
|
||||
stats: dto.statistics,
|
||||
color: dto.color?.asColor ?? borderGray,
|
||||
time: dto.time,
|
||||
timeSubtitle: dto.timeSubtitle,
|
||||
onTap: () {
|
||||
|
|
@ -199,11 +202,10 @@ class _TestsSection extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _AdTile extends StatelessWidget {
|
||||
const _AdTile({super.key});
|
||||
const _AdTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).textTheme;
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final banner = YandexAds.createBanner(
|
||||
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)));
|
||||
|
|
@ -265,13 +267,18 @@ class _TestButton extends StatelessWidget {
|
|||
final String? subtitle;
|
||||
final String? time;
|
||||
final String? timeSubtitle;
|
||||
final Color color;
|
||||
final TestStatisticsDto? stats;
|
||||
|
||||
_TestButton({
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
required this.color,
|
||||
this.subtitle,
|
||||
this.time,
|
||||
this.timeSubtitle,
|
||||
this.stats,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -312,7 +319,9 @@ class _TestButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
|
|
@ -320,6 +329,24 @@ class _TestButton extends StatelessWidget {
|
|||
height: 0.85,
|
||||
),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
if (stats != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 50.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: HorizontalProgressWidget(
|
||||
stats!.words.incorrect.round(),
|
||||
stats!.words.total.round(),
|
||||
color,
|
||||
hasBorder: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,16 +7,25 @@ class HorizontalProgressWidget extends StatelessWidget {
|
|||
final int value;
|
||||
final int length;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
|
||||
HorizontalProgressWidget(this.value, this.length, this.color);
|
||||
HorizontalProgressWidget(
|
||||
this.value,
|
||||
this.length,
|
||||
this.color, {
|
||||
this.hasBorder = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16.h),
|
||||
child: Container(
|
||||
return Container(
|
||||
height: 16.h,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: white,
|
||||
border: hasBorder ? Border.all(color: color) : null,
|
||||
borderRadius: BorderRadius.circular(16.h),
|
||||
),
|
||||
child: AnimatedFractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: length == 0 ? 0 : value.toDouble() / length,
|
||||
|
|
@ -25,7 +34,6 @@ class HorizontalProgressWidget extends StatelessWidget {
|
|||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/features/tests/test_manager.dart';
|
||||
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
|
||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../domain/router/app_router.gr.dart';
|
||||
import '../managers/repository/repository.dart';
|
||||
import '../theme/themes.dart';
|
||||
import 'header.dart';
|
||||
|
||||
class TestsMenuWidget extends StatefulWidget {
|
||||
final Repository repository;
|
||||
final TestManager testManager;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const TestsMenuWidget(
|
||||
this.repository,
|
||||
this.testManager, {
|
||||
this.scrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TestsMenuWidgetState();
|
||||
}
|
||||
|
||||
class _TestsMenuWidgetState extends State<TestsMenuWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final cardWidth = 125.w;
|
||||
final cardHeight = cardWidth;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FutureBuilder(
|
||||
future: widget.testManager.loadTests(),
|
||||
builder: (context, snapshot) {
|
||||
return Container(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
controller: widget.scrollController,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Header('Test'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
height: cardHeight + 8,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
if (snapshot.data == null)
|
||||
...[1, 2, 3, 4].map(
|
||||
(e) => _PackButtonShimmer(
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
),
|
||||
),
|
||||
if (snapshot.hasData)
|
||||
...snapshot.data!.map((dto) =>
|
||||
_TestButton(dto, cardWidth, cardHeight)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _precache(List<CardPackDto> packs) async {
|
||||
for (final pack in packs) {
|
||||
// if (pack.dto != null) {
|
||||
// try {
|
||||
// await precacheImage(MemoryImage(pack.cover!), context);
|
||||
// } catch (e) {
|
||||
// log('${pack.dto.id} cover not cached');
|
||||
// }
|
||||
// }
|
||||
// for (final card in pack.cards.take(3)) {
|
||||
// try {
|
||||
// await precacheImage(card.image, context);
|
||||
// } catch (_) {
|
||||
// log('${pack.dto.id} - ${card.dto.id} image not cached');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryTitle extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
_CategoryTitle(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 28.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: menuBlue,
|
||||
);
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: theme,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TestButton extends StatelessWidget {
|
||||
final TestDto dto;
|
||||
final double cardWidth;
|
||||
final double cardHeight;
|
||||
|
||||
const _TestButton(
|
||||
this.dto,
|
||||
this.cardWidth,
|
||||
this.cardHeight,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(
|
||||
PageRouteInfo(TestPage.name, args: TestPageArgs(testId: dto.id!)),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: dto.color?.asColor,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 4.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: (dto.cover != null)
|
||||
? TestImageWidget(TestImage.image(dto.cover!))
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'18',
|
||||
style: textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
Text('из',
|
||||
style: textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.w800)),
|
||||
Text(
|
||||
'32',
|
||||
style: textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
dto.name,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PackButtonShimmer extends StatelessWidget {
|
||||
final double cardWidth;
|
||||
final double cardHeight;
|
||||
|
||||
const _PackButtonShimmer(
|
||||
this.cardWidth,
|
||||
this.cardHeight,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.white,
|
||||
highlightColor: Colors.grey[200]!,
|
||||
child: Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: cardWidth,
|
||||
height: cardWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
)),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
color: Colors.white,
|
||||
),
|
||||
width: cardWidth * 0.7,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue