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:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../widgets/image_fade.dart';
|
import '../widgets/image_fade.dart';
|
||||||
|
|
||||||
|
|
@ -256,7 +257,10 @@ class AddPackage with Api {
|
||||||
'Preview cards',
|
'Preview cards',
|
||||||
cardPackDto.previewCards?.toString(),
|
cardPackDto.previewCards?.toString(),
|
||||||
(v) {
|
(v) {
|
||||||
final cards = (jsonDecode(v) as List).cast<String>();
|
final cards = v
|
||||||
|
.decodeList<int>()
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.toList();
|
||||||
cardPackDto = cardPackDto.copyWith.previewCards(cards);
|
cardPackDto = cardPackDto.copyWith.previewCards(cards);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
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/managers/repository/api.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
|
||||||
class AdminApi with Api {
|
class AdminApi with Api {
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
|
||||||
|
|
@ -41,10 +41,7 @@ class AdminApi with Api {
|
||||||
'$path/cards',
|
'$path/cards',
|
||||||
queryParameters: {'ids': ids.join(',')},
|
queryParameters: {'ids': ids.join(',')},
|
||||||
);
|
);
|
||||||
return (jsonDecode(r.data!) as List)
|
return (r.data as String).decodeList(GameCardDto.fromJson);
|
||||||
.map((e) => GameCardDto.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList()
|
|
||||||
.cast();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<UserDto>> getUsers(List<String> ids) async {
|
Future<List<UserDto>> getUsers(List<String> ids) async {
|
||||||
|
|
@ -94,9 +91,7 @@ class AdminApi with Api {
|
||||||
final r = await _dio.get<String>(
|
final r = await _dio.get<String>(
|
||||||
'$path/pack/edit/$id',
|
'$path/pack/edit/$id',
|
||||||
);
|
);
|
||||||
return EditCardPackDto.fromJson(
|
return (r.data as String).decode(EditCardPackDto.fromJson);
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> deletePack(EditCardPackDto dto) async {
|
Future<bool> deletePack(EditCardPackDto dto) async {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|
@ -27,11 +28,12 @@ class PackCacheManager {
|
||||||
final dir = await _packDirectory(packId);
|
final dir = await _packDirectory(packId);
|
||||||
final file = File('${dir.path}/pack.json');
|
final file = File('${dir.path}/pack.json');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
final json = jsonEncode(await file.readAsString()) as Map;
|
final json = (await file.readAsString()).decode<Map>();
|
||||||
json['id'] = '0';
|
json['id'] = '0';
|
||||||
await file.writeAsString(jsonEncode(json));
|
await file.writeAsString(jsonEncode(json));
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
log('Cant marck pack for update', error: e, stackTrace: s);
|
||||||
Analytics.crashlyticsError(
|
Analytics.crashlyticsError(
|
||||||
e: e,
|
e: e,
|
||||||
s: s,
|
s: s,
|
||||||
|
|
@ -52,9 +54,7 @@ class PackCacheManager {
|
||||||
log('Pack file not exist');
|
log('Pack file not exist');
|
||||||
return throw Exception('Pack file not exist');
|
return throw Exception('Pack file not exist');
|
||||||
}
|
}
|
||||||
var pack = CardPackDto.fromJson(
|
var pack = packFile.readAsStringSync().decode(CardPackDto.fromJson);
|
||||||
jsonDecode(packFile.readAsStringSync()),
|
|
||||||
);
|
|
||||||
if (withCardImages) {
|
if (withCardImages) {
|
||||||
Map<String, MemoryImage> images = {};
|
Map<String, MemoryImage> images = {};
|
||||||
log('Cards ${packId}: ${pack.cards.length}');
|
log('Cards ${packId}: ${pack.cards.length}');
|
||||||
|
|
@ -147,7 +147,7 @@ class PackCacheManager {
|
||||||
}) async {
|
}) async {
|
||||||
log('Saving pack ${dto.id}');
|
log('Saving pack ${dto.id}');
|
||||||
final dir = await _packDirectory(dto.id);
|
final dir = await _packDirectory(dto.id);
|
||||||
final json = jsonEncode(dto);
|
final json = dto.encode();
|
||||||
log('Saving dto ${dto.id}');
|
log('Saving dto ${dto.id}');
|
||||||
File('${dir.path}/pack.json')
|
File('${dir.path}/pack.json')
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -19,10 +18,8 @@ class PacksApi with Api {
|
||||||
'$path/packs/previews',
|
'$path/packs/previews',
|
||||||
queryParameters: packData,
|
queryParameters: packData,
|
||||||
);
|
);
|
||||||
final previews = (jsonDecode(r.data!) as List<dynamic>)
|
final previews =
|
||||||
.cast<Map<String, dynamic>>()
|
(r.data! as String).decodeList(CardPackPreviewDto.fromJson).toList();
|
||||||
.map(CardPackPreviewDto.fromJson)
|
|
||||||
.toList();
|
|
||||||
return previews;
|
return previews;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,10 +31,7 @@ class PacksApi with Api {
|
||||||
'$path/packs/actions',
|
'$path/packs/actions',
|
||||||
queryParameters: packData,
|
queryParameters: packData,
|
||||||
);
|
);
|
||||||
final actions = (jsonDecode(r.data!) as List<dynamic>)
|
final actions = (r.data! as String).decodeList(CardPackAction.fromJson);
|
||||||
.cast<Map<String, Object?>>()
|
|
||||||
.map(CardPackAction.fromJson)
|
|
||||||
.toList();
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,9 +44,7 @@ class PacksApi with Api {
|
||||||
receiveTimeout: Duration(seconds: 5),
|
receiveTimeout: Duration(seconds: 5),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return CardPackDto.fromJson(
|
return (r.data as String).decode(CardPackDto.fromJson);
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buy page dto if not available
|
// buy page dto if not available
|
||||||
|
|
@ -64,9 +56,7 @@ class PacksApi with Api {
|
||||||
receiveTimeout: Duration(seconds: 5),
|
receiveTimeout: Duration(seconds: 5),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return CardPackBuyDto.fromJson(
|
return (r.data as String).decode(CardPackBuyDto.fromJson);
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// cards zip archive for available pack
|
/// cards zip archive for available pack
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|
@ -30,9 +31,7 @@ class PreviewPackHolder {
|
||||||
final List<CardPackPreviewDto> previewPacks = [];
|
final List<CardPackPreviewDto> previewPacks = [];
|
||||||
previewPacksStrings.forEach((string) {
|
previewPacksStrings.forEach((string) {
|
||||||
try {
|
try {
|
||||||
final pack = CardPackPreviewDto.fromJson(
|
final pack = string.decode(CardPackPreviewDto.fromJson);
|
||||||
jsonDecode(string) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
previewPacks.add(pack);
|
previewPacks.add(pack);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log('Cant load preview pack', error: e, stackTrace: 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:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:mnemo_cards/flags.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
import '../../theme/themes.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.isCorrect = false,
|
||||||
this.isAnswered = false,
|
this.isAnswered = false,
|
||||||
this.isSkipped = 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:mnemo_cards/utils/color_helper.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
import '../../di/locator.dart';
|
||||||
import '../../widgets/big_back_button.dart';
|
import '../../widgets/big_back_button.dart';
|
||||||
import '../../widgets/horizontal_progress.dart';
|
import '../../widgets/horizontal_progress.dart';
|
||||||
import 'question_states/test_question_state.dart';
|
import 'question_states/test_question_state.dart';
|
||||||
|
|
@ -37,132 +38,143 @@ class TestCompleteWidget extends StatelessWidget {
|
||||||
)
|
)
|
||||||
.values
|
.values
|
||||||
.toList()
|
.toList()
|
||||||
..sort((p, n) => p.correctCount.compareTo(n.correctCount));
|
..sort(
|
||||||
return Column(
|
(p, n) => (p.correctCount.toDouble() / p.totalCount)
|
||||||
children: [
|
.compareTo(n.correctCount.toDouble() / n.totalCount),
|
||||||
Expanded(
|
);
|
||||||
flex: 3,
|
return PopScope(
|
||||||
child: Padding(
|
onPopInvoked: (v) {
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
if (v) {
|
||||||
child: Column(
|
locator.testManager.sendStatistics();
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
locator.testManager.triggerPoll();
|
||||||
children: [
|
}
|
||||||
Container(
|
},
|
||||||
width: MediaQuery.of(context).size.width / 1.8,
|
child: Column(
|
||||||
height: MediaQuery.of(context).size.width / 1.8,
|
children: [
|
||||||
padding: const EdgeInsets.all(20.0),
|
Expanded(
|
||||||
child: StreamBuilder<int>(
|
flex: 3,
|
||||||
stream: Stream.fromIterable([5])
|
child: Padding(
|
||||||
.interval(Duration(milliseconds: 0)),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
initialData: 0,
|
child: Column(
|
||||||
builder: (context, snapshot) {
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
final a = min(snapshot.requireData / 5.0, 1.0);
|
children: [
|
||||||
return PieChart(
|
Container(
|
||||||
key: _pieChartKey,
|
width: MediaQuery.of(context).size.width / 1.8,
|
||||||
swapAnimationDuration: Duration(milliseconds: 600),
|
height: MediaQuery.of(context).size.width / 1.8,
|
||||||
PieChartData(
|
padding: const EdgeInsets.all(20.0),
|
||||||
pieTouchData: PieTouchData(),
|
child: StreamBuilder<int>(
|
||||||
borderData: FlBorderData(
|
stream: Stream.fromIterable([5])
|
||||||
show: false,
|
.interval(Duration(milliseconds: 0)),
|
||||||
|
initialData: 0,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final a = min(snapshot.requireData / 5.0, 1.0);
|
||||||
|
return PieChart(
|
||||||
|
key: _pieChartKey,
|
||||||
|
swapAnimationDuration: Duration(milliseconds: 600),
|
||||||
|
PieChartData(
|
||||||
|
pieTouchData: PieTouchData(),
|
||||||
|
borderData: FlBorderData(
|
||||||
|
show: false,
|
||||||
|
),
|
||||||
|
sectionsSpace: 0,
|
||||||
|
centerSpaceRadius:
|
||||||
|
MediaQuery.of(context).size.width / 8,
|
||||||
|
sections: [
|
||||||
|
PieChartSectionData(
|
||||||
|
color: successColor,
|
||||||
|
showTitle: false,
|
||||||
|
value: correct.toDouble() * a,
|
||||||
|
badgeWidget: Image.asset(
|
||||||
|
'icons/crown.png',
|
||||||
|
width: 20.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PieChartSectionData(
|
||||||
|
color: successColor.lighten(0.15),
|
||||||
|
showTitle: false,
|
||||||
|
value: incorrect.toDouble() * a,
|
||||||
|
badgeWidget: Image.asset(
|
||||||
|
'icons/skull.png',
|
||||||
|
width: 20.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PieChartSectionData(
|
||||||
|
color: Colors.white,
|
||||||
|
showTitle: false,
|
||||||
|
value: skipped.toDouble(),
|
||||||
|
badgeWidget: Image.asset(
|
||||||
|
'icons/small_circle.png',
|
||||||
|
width: 20.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
sectionsSpace: 0,
|
);
|
||||||
centerSpaceRadius:
|
}),
|
||||||
MediaQuery.of(context).size.width / 8,
|
|
||||||
sections: [
|
|
||||||
PieChartSectionData(
|
|
||||||
color: successColor,
|
|
||||||
showTitle: false,
|
|
||||||
value: correct.toDouble() * a,
|
|
||||||
badgeWidget: Image.asset(
|
|
||||||
'icons/crown.png',
|
|
||||||
width: 20.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PieChartSectionData(
|
|
||||||
color: successColor.lighten(0.15),
|
|
||||||
showTitle: false,
|
|
||||||
value: incorrect.toDouble() * a,
|
|
||||||
badgeWidget: Image.asset(
|
|
||||||
'icons/skull.png',
|
|
||||||
width: 20.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PieChartSectionData(
|
|
||||||
color: Colors.white,
|
|
||||||
showTitle: false,
|
|
||||||
value: skipped.toDouble(),
|
|
||||||
badgeWidget: Image.asset(
|
|
||||||
'icons/small_circle.png',
|
|
||||||
width: 20.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$correct/$total',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 36,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
'$correct/$total',
|
||||||
'1:47',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 36,
|
||||||
fontSize: 20,
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
height: 0.85,
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
'1:47',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 0.85,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
flex: 2,
|
||||||
flex: 2,
|
child: Container(
|
||||||
child: Container(
|
alignment: Alignment.bottomCenter,
|
||||||
alignment: Alignment.bottomCenter,
|
child: ListView.builder(
|
||||||
child: ListView.builder(
|
shrinkWrap: true,
|
||||||
shrinkWrap: true,
|
itemCount: words.length,
|
||||||
itemCount: words.length,
|
itemBuilder: (context, index) {
|
||||||
itemBuilder: (context, index) {
|
final word = words[index];
|
||||||
final word = words[index];
|
return Padding(
|
||||||
return Padding(
|
padding: const EdgeInsets.only(
|
||||||
padding: const EdgeInsets.only(
|
left: 12.0,
|
||||||
left: 12.0,
|
right: 24,
|
||||||
right: 24,
|
),
|
||||||
),
|
child: Row(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
'${word.word}',
|
||||||
'${word.word}',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 24,
|
||||||
fontSize: 24,
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 2.5,
|
|
||||||
child: HorizontalProgressWidget(
|
|
||||||
word.correctCount,
|
|
||||||
word.totalCount,
|
|
||||||
successColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Flexible(
|
||||||
],
|
child: SizedBox(
|
||||||
),
|
width: MediaQuery.of(context).size.width / 2.5,
|
||||||
);
|
child: HorizontalProgressWidget(
|
||||||
},
|
word.correctCount,
|
||||||
|
word.totalCount,
|
||||||
|
successColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const BigBackButton(),
|
||||||
const BigBackButton(),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
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_state_holder.dart';
|
||||||
import 'package:mnemo_cards/features/tests/test_widgets/test_image.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/features/packs/pack_cache_manager.dart';
|
||||||
|
|
@ -18,15 +19,22 @@ import 'question_states/test_question_state.dart';
|
||||||
class TestManager extends ChangeNotifier {
|
class TestManager extends ChangeNotifier {
|
||||||
final PackCacheManager packCacheManager;
|
final PackCacheManager packCacheManager;
|
||||||
final HttpRepository _repository;
|
final HttpRepository _repository;
|
||||||
final List<AbstractTestQuestion> questions = [];
|
|
||||||
Color color = borderGray;
|
|
||||||
final PageController pageController = PageController();
|
final PageController pageController = PageController();
|
||||||
final TestHolder testHolder = TestHolder();
|
final TestHolder testHolder = TestHolder();
|
||||||
|
final StreamController<void> _triggerController =
|
||||||
|
StreamController.broadcast();
|
||||||
|
|
||||||
Future<List<TestDto>> loadTests({String? packId}) async {
|
TestDto? _test;
|
||||||
final tests = (await _repository.getTests(packId: packId ?? ''))
|
|
||||||
.map((e) => TestDto.fromJson(e))
|
Color get color => _test?.color?.asColor ?? borderGray;
|
||||||
.toList();
|
|
||||||
|
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;
|
return tests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,12 +44,16 @@ class TestManager extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<TestDto>> loadTestsStream({String? packId}) {
|
Stream<List<TestDto>> loadTestsStream({String? packId}) {
|
||||||
return Stream.periodic(
|
return Rx.combineLatest2(
|
||||||
Duration(seconds: 60),
|
_triggerController.stream.startWith(null),
|
||||||
|
Stream.periodic(
|
||||||
|
Duration(seconds: 120),
|
||||||
|
).startWith(null),
|
||||||
|
(_, __) => null,
|
||||||
)
|
)
|
||||||
.startWith(null)
|
.throttleTime(Duration(seconds: 1))
|
||||||
.asyncMap(
|
.asyncMap(
|
||||||
(tests) async => loadTests(packId: packId)
|
(tests) async => pollTests(packId: packId)
|
||||||
.catchError((_) => Future.value(<TestDto>[])),
|
.catchError((_) => Future.value(<TestDto>[])),
|
||||||
)
|
)
|
||||||
.where((test) => test.isNotEmpty);
|
.where((test) => test.isNotEmpty);
|
||||||
|
|
@ -56,19 +68,20 @@ class TestManager extends ChangeNotifier {
|
||||||
if (packId != null) {
|
if (packId != null) {
|
||||||
await packCacheManager.loadPackFromCache(packId);
|
await packCacheManager.loadPackFromCache(packId);
|
||||||
}
|
}
|
||||||
TestDto? test;
|
|
||||||
try {
|
try {
|
||||||
test = await _repository.getTest(id);
|
_test = await _repository.getTest(id);
|
||||||
} on Object catch (e, s) {
|
} 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();
|
appRouter.maybePop();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
questions.clear();
|
// todo use uuid
|
||||||
questions.addAll(test!.questions);
|
testHolder.setQuestions(
|
||||||
color = test.color?.asColor ?? borderGray;
|
_test!.questions,
|
||||||
testHolder.setQuestions(questions);
|
DateTime.timestamp().toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setState(int id, TestQuestionState state) {
|
void setState(int id, TestQuestionState state) {
|
||||||
|
|
@ -153,4 +166,28 @@ class TestManager extends ChangeNotifier {
|
||||||
final bytes = await packCacheManager.loadPackFile(packId, path);
|
final bytes = await packCacheManager.loadPackFile(packId, path);
|
||||||
return MemoryImage(bytes!);
|
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 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import 'scroll_physics.dart';
|
import 'scroll_physics.dart';
|
||||||
|
|
@ -26,6 +29,9 @@ class TestPage extends StatefulWidget {
|
||||||
class _TestPageState extends State<TestPage> {
|
class _TestPageState extends State<TestPage> {
|
||||||
PageController get pageController => locator.testManager.pageController;
|
PageController get pageController => locator.testManager.pageController;
|
||||||
|
|
||||||
|
int? lastSkipIndex;
|
||||||
|
int? _lastAudioQuestionId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -41,25 +47,46 @@ class _TestPageState extends State<TestPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pageScrollListener() {
|
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) {
|
if (index > 0 && index <= locator.testManager.questions.length) {
|
||||||
final prevId = locator.testManager.questions[index - 1].id!;
|
final prevId = locator.testManager.questions[index - 1].id!;
|
||||||
final state = locator.testManager.getState(prevId);
|
final state = locator.testManager.getState(prevId);
|
||||||
if (state?.isAnswered == false) {
|
if (state?.isAnswered == false) {
|
||||||
// log('Question skipped');
|
print('Skipping $index ${state?.word}');
|
||||||
locator.testManager.skip(prevId);
|
locator.testManager.skip(prevId);
|
||||||
|
lastSkipIndex = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int lastTestIndex = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: StreamBuilder(
|
body: StreamBuilder(
|
||||||
stream: locator.testManager.testHolder.asStream,
|
stream: locator.testManager.testHolder.asStream.takeWhileInclusive(
|
||||||
|
(state) => state?.questions.isNotEmpty != true,
|
||||||
|
),
|
||||||
builder: (context, s) {
|
builder: (context, s) {
|
||||||
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
||||||
return Scaffold(
|
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(
|
return Scaffold(
|
||||||
backgroundColor: locator.testManager.color.withOpacity(0.25),
|
backgroundColor: locator.testManager.color.withOpacity(0.25),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
@ -100,27 +137,55 @@ class _TestPageState extends State<TestPage> {
|
||||||
locator.testManager.color,
|
locator.testManager.color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
final question = locator.testManager.getTest(index);
|
||||||
lastTestIndex = math.max(index, lastTestIndex);
|
return switch (question.questionType) {
|
||||||
return switch (locator.testManager
|
|
||||||
.getTest(index)
|
|
||||||
.questionType) {
|
|
||||||
TestQuestionType.simple => SimpleTestWidget(
|
TestQuestionType.simple => SimpleTestWidget(
|
||||||
locator.testManager.getTest(index)
|
question as SimpleTestQuestionBody,
|
||||||
as SimpleTestQuestionBody,
|
key: keys[index],
|
||||||
),
|
),
|
||||||
TestQuestionType.input_buttons =>
|
TestQuestionType.input_buttons =>
|
||||||
InputButtonsTestWidget(
|
InputButtonsTestWidget(
|
||||||
locator.testManager.getTest(index)
|
question as InputButtonsTestQuestionBody,
|
||||||
as InputButtonsTestQuestionBody,
|
key: keys[index]),
|
||||||
),
|
|
||||||
TestQuestionType.matrix => Container(),
|
TestQuestionType.matrix => Container(),
|
||||||
TestQuestionType.match => Container(),
|
TestQuestionType.match => Container(),
|
||||||
// TODO: Handle this case.
|
// TODO: Handle this case.
|
||||||
TestQuestionType.undefined =>
|
TestQuestionType.undefined =>
|
||||||
throw UnimplementedError(),
|
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;
|
Stream<Duration>? timer;
|
||||||
|
|
||||||
Timer? _updateTimer;
|
bool _active = false;
|
||||||
|
|
||||||
void _updateDataListener() {
|
void _updateDataListener() {
|
||||||
if (_updateTimer == null || !_updateTimer!.isActive) {
|
if (!_active) {
|
||||||
_updateTimer = Timer(Duration(milliseconds: 100), _updateData);
|
_active = true;
|
||||||
|
Timer(Duration(milliseconds: 100), () => _updateData());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,16 +54,16 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
||||||
results.add(Result.not_visited);
|
results.add(Result.not_visited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print('Update data ${results.map((e) => e.name).join(' ')}');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
_active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// locator.testManager.removeListener(_updateData);
|
|
||||||
// locator.testManager.addListener(_updateData);
|
|
||||||
locator.testManager.pageController.removeListener(_updateDataListener);
|
locator.testManager.pageController.removeListener(_updateDataListener);
|
||||||
locator.testManager.pageController.addListener(_updateDataListener);
|
locator.testManager.pageController.addListener(_updateDataListener);
|
||||||
timer = Stream.periodic(
|
timer = Stream.periodic(
|
||||||
|
|
@ -79,7 +80,6 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
||||||
length,
|
length,
|
||||||
locator.testManager.color,
|
locator.testManager.color,
|
||||||
timer: timer,
|
timer: timer,
|
||||||
key: ValueKey('TEST PROGRESS'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,20 @@ class TestHolder {
|
||||||
StreamController.broadcast();
|
StreamController.broadcast();
|
||||||
_TestState __state;
|
_TestState __state;
|
||||||
|
|
||||||
TestHolder() : __state = _TestState({});
|
TestHolder() : __state = _TestState({}, null);
|
||||||
|
|
||||||
void skip(int id) {
|
void skip(int id) {
|
||||||
final state = testQuestionState(id);
|
final state = testQuestionState(id);
|
||||||
|
print('Skipping $id ${state} ${state?.testType}');
|
||||||
switch (state?.testType) {
|
switch (state?.testType) {
|
||||||
case TestQuestionType.simple:
|
case TestQuestionType.simple:
|
||||||
state as SimpleTestState;
|
state as SimpleTestState;
|
||||||
|
print('Skipped $id ${state.word}');
|
||||||
setQuestionState(id, state.copyWith(isSkipped: true));
|
setQuestionState(id, state.copyWith(isSkipped: true));
|
||||||
break;
|
break;
|
||||||
case TestQuestionType.input_buttons:
|
case TestQuestionType.input_buttons:
|
||||||
state as InputButtonsTestState;
|
state as InputButtonsTestState;
|
||||||
|
print('Skipped $id ${state.word}');
|
||||||
setQuestionState(id, state.copyWith(isSkipped: true));
|
setQuestionState(id, state.copyWith(isSkipped: true));
|
||||||
break;
|
break;
|
||||||
case TestQuestionType.matrix:
|
case TestQuestionType.matrix:
|
||||||
|
|
@ -40,16 +43,20 @@ class TestHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setQuestionState(int id, TestQuestionState state) => _state = _TestState(
|
void setQuestionState(int id, TestQuestionState state) {
|
||||||
Map.from(_state.questions..[id] = state),
|
_state = _TestState(
|
||||||
);
|
Map.from(_state.questions..[id] = state),
|
||||||
|
_state.sessionToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
_state = _TestState({});
|
_state = _TestState({}, null);
|
||||||
_streamController.add(_state);
|
_streamController.add(_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setQuestions(List<AbstractTestQuestion> questions) {
|
void setQuestions(
|
||||||
|
List<AbstractTestQuestion> questions, String? sessionToken) {
|
||||||
final stateQuestions = <int, TestQuestionState>{};
|
final stateQuestions = <int, TestQuestionState>{};
|
||||||
final seed = Random().nextInt(999);
|
final seed = Random().nextInt(999);
|
||||||
for (final question in questions) {
|
for (final question in questions) {
|
||||||
|
|
@ -72,7 +79,7 @@ class TestHolder {
|
||||||
// TODO: Handle this case.
|
// TODO: Handle this case.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_state = _TestState(stateQuestions);
|
_state = _TestState(stateQuestions, sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
TestQuestionState? testQuestionState(int id) => _state.questions[id];
|
TestQuestionState? testQuestionState(int id) => _state.questions[id];
|
||||||
|
|
@ -91,7 +98,8 @@ class TestHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TestState {
|
class _TestState {
|
||||||
|
final String? sessionToken;
|
||||||
final Map<int, TestQuestionState> questions;
|
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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_tts/flutter_tts.dart';
|
import 'package:flutter_tts/flutter_tts.dart';
|
||||||
|
import 'package:mnemo_cards/managers/audio_player.dart';
|
||||||
|
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../widgets/game_card_widget.dart';
|
import '../../../widgets/game_card_widget.dart';
|
||||||
|
|
@ -11,15 +12,17 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
||||||
final String? image;
|
final String? image;
|
||||||
final String? audio;
|
final String? audio;
|
||||||
|
|
||||||
CommonTestQuestionWidget({
|
const CommonTestQuestionWidget({
|
||||||
this.text,
|
this.text,
|
||||||
this.image,
|
this.image,
|
||||||
this.audio,
|
this.audio,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context).textTheme;
|
final theme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -49,7 +52,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
||||||
'icons/sound_on.png',
|
'icons/sound_on.png',
|
||||||
width: 26,
|
width: 26,
|
||||||
),
|
),
|
||||||
onPressed: () => _playAudio(),
|
onPressed: () => AudioPlayer.playAudio(audio),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -66,7 +69,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
),
|
),
|
||||||
onPressed: () => _playAudio(),
|
onPressed: () => AudioPlayer.playAudio(audio),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -91,7 +94,7 @@ class CommonTestQuestionWidget extends StatelessWidget {
|
||||||
width: 26,
|
width: 26,
|
||||||
height: theme.headlineLarge?.fontSize,
|
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;
|
Color get testColor => manager.color;
|
||||||
|
|
||||||
SimpleTestWidget(this._simpleTestModel, {super.key});
|
const SimpleTestWidget(this._simpleTestModel, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final random = Random(state.seed);
|
final random = Random(state.seed);
|
||||||
final buttons = [...model.buttons]..shuffle(random);
|
final buttons = [...model.buttons]..shuffle(random);
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
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:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import 'package:dio/dio.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/repository/repository.dart';
|
||||||
import 'package:mnemo_cards/managers/user_manager.dart';
|
import 'package:mnemo_cards/managers/user_manager.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.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 'api.dart';
|
||||||
import 'firebase_config_repository.dart';
|
|
||||||
|
|
||||||
class HttpRepository extends Repository with Api {
|
class HttpRepository extends Repository with Api {
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
|
||||||
HttpRepository(this._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
|
@override
|
||||||
Future<UserDto> get user async => UserDto.empty;
|
Future<UserDto> get user async => UserDto.empty;
|
||||||
|
|
||||||
|
|
@ -58,12 +37,7 @@ class HttpRepository extends Repository with Api {
|
||||||
});
|
});
|
||||||
//todo add client header encryption ??
|
//todo add client header encryption ??
|
||||||
final authToken = r.headers[HttpHeaders.authorizationHeader]!.last;
|
final authToken = r.headers[HttpHeaders.authorizationHeader]!.last;
|
||||||
return (
|
return ((r.data as String).decode(UserDto.fromJson), authToken);
|
||||||
UserDto.fromJson(
|
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
),
|
|
||||||
authToken,
|
|
||||||
);
|
|
||||||
} on DioException catch (e, s) {
|
} on DioException catch (e, s) {
|
||||||
log('User create error', error: e, stackTrace: s);
|
log('User create error', error: e, stackTrace: s);
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|
@ -83,24 +57,35 @@ class HttpRepository extends Repository with Api {
|
||||||
Future<UserDto?> getUser() async {
|
Future<UserDto?> getUser() async {
|
||||||
final r = await _dio.get<String>('$path/user');
|
final r = await _dio.get<String>('$path/user');
|
||||||
if (r.statusCode == 200) {
|
if (r.statusCode == 200) {
|
||||||
return UserDto.fromJson(
|
return (r.data as String).decode(UserDto.fromJson);
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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
|
@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>(
|
final r = await _dio.get<String>(
|
||||||
'$path/tests/$packId',
|
'$path/tests/$packId',
|
||||||
);
|
);
|
||||||
final ll =
|
return (r.data as String).decodeList(TestDto.fromJson);
|
||||||
(jsonDecode(r.data!) as List<dynamic>).cast<Map<String, Object?>>();
|
|
||||||
return ll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -109,9 +94,7 @@ class HttpRepository extends Repository with Api {
|
||||||
'$path/test/$id',
|
'$path/test/$id',
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
return TestDto.fromJson(
|
return (r.data as String).decode(TestDto.fromJson);
|
||||||
jsonDecode(r.data as String) as Map<String, Object?>,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log(e.toString(), stackTrace: s);
|
log(e.toString(), stackTrace: s);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,13 @@ abstract class Repository {
|
||||||
|
|
||||||
Future<UserDto?> getUser();
|
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);
|
Future<TestDto?> getTest(String id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ class UserManager {
|
||||||
.listen((user) async {
|
.listen((user) async {
|
||||||
try {
|
try {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
AppRouter.openAuthOrProfile();
|
AppRouter.openAuthOrProfile();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
AppRouter.closeAuthPage();
|
AppRouter.closeAuthPage();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import '../../features/yandex_ads/yandex_ads.dart';
|
||||||
|
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../theme/themes.dart';
|
import '../../theme/themes.dart';
|
||||||
|
import '../horizontal_progress.dart';
|
||||||
import 'card_pack_header.dart';
|
import 'card_pack_header.dart';
|
||||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||||
|
|
||||||
|
|
@ -180,6 +181,8 @@ class _TestsSection extends StatelessWidget {
|
||||||
child: _TestButton(
|
child: _TestButton(
|
||||||
title: dto.name,
|
title: dto.name,
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
|
stats: dto.statistics,
|
||||||
|
color: dto.color?.asColor ?? borderGray,
|
||||||
time: dto.time,
|
time: dto.time,
|
||||||
timeSubtitle: dto.timeSubtitle,
|
timeSubtitle: dto.timeSubtitle,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
@ -199,11 +202,10 @@ class _TestsSection extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdTile extends StatelessWidget {
|
class _AdTile extends StatelessWidget {
|
||||||
const _AdTile({super.key});
|
const _AdTile();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context).textTheme;
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
final banner = YandexAds.createBanner(
|
final banner = YandexAds.createBanner(
|
||||||
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)));
|
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)));
|
||||||
|
|
@ -265,13 +267,18 @@ class _TestButton extends StatelessWidget {
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
final String? time;
|
final String? time;
|
||||||
final String? timeSubtitle;
|
final String? timeSubtitle;
|
||||||
|
final Color color;
|
||||||
|
final TestStatisticsDto? stats;
|
||||||
|
|
||||||
_TestButton({
|
_TestButton({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
required this.color,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.time,
|
this.time,
|
||||||
this.timeSubtitle,
|
this.timeSubtitle,
|
||||||
|
this.stats,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -312,12 +319,32 @@ class _TestButton extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (subtitle != null)
|
if (subtitle != null)
|
||||||
Text(
|
Padding(
|
||||||
subtitle!,
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 16.sp,
|
subtitle!,
|
||||||
fontWeight: FontWeight.w300,
|
style: TextStyle(
|
||||||
height: 0.85,
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
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,23 +7,31 @@ class HorizontalProgressWidget extends StatelessWidget {
|
||||||
final int value;
|
final int value;
|
||||||
final int length;
|
final int length;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final bool hasBorder;
|
||||||
|
|
||||||
HorizontalProgressWidget(this.value, this.length, this.color);
|
HorizontalProgressWidget(
|
||||||
|
this.value,
|
||||||
|
this.length,
|
||||||
|
this.color, {
|
||||||
|
this.hasBorder = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return Container(
|
||||||
borderRadius: BorderRadius.circular(16.h),
|
height: 16.h,
|
||||||
child: Container(
|
clipBehavior: Clip.antiAlias,
|
||||||
height: 16.h,
|
decoration: BoxDecoration(
|
||||||
color: white,
|
color: white,
|
||||||
child: AnimatedFractionallySizedBox(
|
border: hasBorder ? Border.all(color: color) : null,
|
||||||
alignment: Alignment.centerLeft,
|
borderRadius: BorderRadius.circular(16.h),
|
||||||
widthFactor: length == 0 ? 0 : value.toDouble() / length,
|
),
|
||||||
duration: Duration(milliseconds: 600),
|
child: AnimatedFractionallySizedBox(
|
||||||
child: Container(
|
alignment: Alignment.centerLeft,
|
||||||
color: color,
|
widthFactor: length == 0 ? 0 : value.toDouble() / length,
|
||||||
),
|
duration: Duration(milliseconds: 600),
|
||||||
|
child: Container(
|
||||||
|
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