This commit is contained in:
Dmitry 2024-06-05 05:41:14 +03:00
parent 6919ef1e6c
commit 69afc51781
7 changed files with 259 additions and 203 deletions

View file

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../../../main.dart';
import '../../../widgets/game_card_widget.dart';
import 'test_image.dart';
class CommonTestQuestionWidget extends StatelessWidget {
final String? text;
final String? image;
final String? audio;
CommonTestQuestionWidget({
this.text,
this.image,
this.audio,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context).textTheme;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (image != null)
Flexible(
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(GameCardWidget.borderRadius),
),
child: AspectRatio(
aspectRatio: text == null ? 1 : 1.5,
child: Stack(
alignment: Alignment.center,
children: [
TestImageWidget(
TestImage.image(image!),
),
if (audio != null && text == null)
Container(
padding: EdgeInsets.all(4.0.h),
alignment: Alignment.topRight,
child: IconButton(
icon: Image.asset(
'icons/sound_on.png',
width: 26,
),
onPressed: () => _playAudio(),
),
),
],
),
),
),
)
else if (text == null && audio != null)
Flexible(
child: Center(
child: IconButton(
icon: Image.asset(
'icons/sound_on.png',
width: 80,
height: 80,
),
onPressed: () => _playAudio(),
),
),
),
if (text != null)
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
text!,
style: theme.headlineLarge,
),
if (audio != null)
Container(
padding: EdgeInsets.only(left: 4.w, top: 4.h),
child: IconButton(
icon: Image.asset(
'icons/sound_on.png',
width: 26,
height: theme.headlineLarge?.fontSize,
),
onPressed: () => _playAudio(),
),
)
],
),
),
],
);
}
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);
}
}
}

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mnemo_cards/features/tests/test_manager.dart'; import 'package:mnemo_cards/features/tests/test_manager.dart';
import 'package:mnemo_cards/features/tests/test_widgets/common_test_question_widget.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/theme/themes.dart'; import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -47,44 +48,13 @@ class InputButtonsTestWidget extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (model.image != null) Expanded(
Container( child: CommonTestQuestionWidget(
margin: const EdgeInsets.all(4.0), text: model.text,
decoration: BoxDecoration( image: model.image,
color: Colors.white, audio: model.audio,
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AspectRatio(
aspectRatio: model.text == null ? 1 : 1.5,
child: TestImageWidget(
TestImage.image(model.image!),
),
),
if (model.text != null)
Container(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Text(
model.text!,
style: theme.headlineLarge,
),
),
),
],
),
)
else
Flexible(
child: Center(
child: Text(
model.text!,
style: theme.headlineLarge,
),
),
), ),
),
StatefulBuilder(builder: (context, setState) { StatefulBuilder(builder: (context, setState) {
void buttonTapped(int index, TestButtonDto button) { void buttonTapped(int index, TestButtonDto button) {
if (state.answer.fold(0, (p, n) => p + n.length) >= if (state.answer.fold(0, (p, n) => p + n.length) >=
@ -118,7 +88,7 @@ class InputButtonsTestWidget extends StatelessWidget {
} }
int splitIndex = 0; int splitIndex = 0;
if (model.template.length * 30 > constraints.maxWidth * 0.9) { if (model.template.length * 32 > constraints.maxWidth * 0.9) {
splitIndex = model.template.indexOf( splitIndex = model.template.indexOf(
' ', ' ',
(model.template.length / 2).ceil(), (model.template.length / 2).ceil(),

View file

@ -2,8 +2,11 @@ import 'dart:math';
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:mnemo_cards/features/tests/test_manager.dart'; import 'package:mnemo_cards/features/tests/test_manager.dart';
import 'package:mnemo_cards/features/tests/test_widgets/common_test_question_widget.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/main.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../../../di/locator.dart'; import '../../../di/locator.dart';
@ -28,7 +31,6 @@ class SimpleTestWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context).textTheme;
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) {
@ -37,34 +39,13 @@ class SimpleTestWidget extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (model.image != null) Expanded(
Flexible( child: CommonTestQuestionWidget(
flex: 4, text: model.text,
child: Container( image: model.image,
margin: const EdgeInsets.all(4.0), audio: model.audio,
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(GameCardWidget.borderRadius),
),
child: TestImageWidget(
TestImage.image(model.image!),
),
),
),
if (model.text != null)
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Center(
child: Text(
model.text!,
style: theme.headlineLarge,
),
),
),
), ),
),
SizedBox( SizedBox(
height: 16.0, height: 16.0,
), ),

View file

@ -50,20 +50,24 @@ class HttpRepository extends Repository with Api {
ExternalIdType idType, ExternalIdType idType,
String? name, String? name,
) async { ) async {
final r = await _dio.post<String>('$path/user/create', data: { try {
'token': externalId, final r = await _dio.post<String>('$path/user/create', data: {
'tokenType': idType.name, 'token': externalId,
'name': name, 'tokenType': idType.name,
}).catchError( 'name': name,
(error, stackTrace) => log('', error: error, stackTrace: stackTrace)); });
//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 (
UserDto.fromJson( UserDto.fromJson(
jsonDecode(r.data as String) as Map<String, Object?>, jsonDecode(r.data as String) as Map<String, Object?>,
), ),
authToken, authToken,
); );
} on DioException catch (e, s) {
log('User create error', error: e, stackTrace: s);
rethrow;
}
} }
void setAuthToken(String? authToken) { void setAuthToken(String? authToken) {

View file

@ -59,6 +59,7 @@ class UserManager {
ExternalIdType idType, ExternalIdType idType,
String? name, String? name,
) async { ) async {
await locator.packCacheManager.clearCache();
final (user, token) = final (user, token) =
await _repository.createOrGetUser(externalId, idType, name); await _repository.createOrGetUser(externalId, idType, name);
_setAuthToken(token); _setAuthToken(token);

View file

@ -33,139 +33,122 @@ class AvailablePack extends StatelessWidget {
Analytics.availablePackOpened(cardPack); Analytics.availablePackOpened(cardPack);
return Column( return Column(
children: [ children: [
CardPackHeader( CardPackHeader.fromDto(cardPack),
cardPack.title,
cardPack.subtitle,
Colors.transparent,
),
Expanded( Expanded(
child: RefreshIndicator( child: ListView(
onRefresh: () => physics: BouncingScrollPhysics(
locator.packUpdater.updatePackAndSaveImages(cardPack.id), decelerationRate: ScrollDecelerationRate.fast,
backgroundColor: Colors.white, parent: AlwaysScrollableScrollPhysics()),
color: cardPack.color?.asColor ?? Colors.black, controller: _mainScrollController,
child: ListView( children: [
physics: BouncingScrollPhysics( StatefulBuilder(builder: (context, setState) {
decelerationRate: ScrollDecelerationRate.fast, return Column(
parent: AlwaysScrollableScrollPhysics()), children: [
controller: _mainScrollController, Container(
children: [ decoration: BoxDecoration(
StatefulBuilder(builder: (context, setState) { color: cardPack.color?.asColor?.withOpacity(0.1),
return Column(
children: [
Container(
height: 1,
color: cardPack.color?.asColor ??
Colors.grey.withOpacity(0.5),
), ),
Container( alignment: Alignment.center,
decoration: BoxDecoration( clipBehavior: Clip.antiAlias,
color: cardPack.color?.asColor?.withOpacity(0.1), child: CardsView(_cardsViewController, cardPack),
), ),
alignment: Alignment.center, Container(
clipBehavior: Clip.antiAlias, height: 1,
child: CardsView(_cardsViewController, cardPack), color: cardPack.color?.asColor ??
Colors.grey.withOpacity(0.5),
),
SizedBox(
height: 10.h,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
), ),
Container( child: Row(
height: 1, mainAxisAlignment: MainAxisAlignment.spaceBetween,
color: cardPack.color?.asColor ?? children: [
Colors.grey.withOpacity(0.5), StreamBuilder(
), stream: _cardsViewController.asStream,
SizedBox( builder: (context, snapshot) {
height: 10.h, return AnimatedRotation(
), duration: Duration(milliseconds: 300),
Padding( turns: (snapshot.data?.expanded ?? false)
padding: EdgeInsets.symmetric( ? 0.5
horizontal: 10.w, : 0.0,
), child: _Button(
child: Row( 'down.png',
mainAxisAlignment: MainAxisAlignment.spaceBetween, () async {
children: [ if (snapshot.hasData) {
StreamBuilder( _cardsViewController.setExpanded(
stream: _cardsViewController.asStream, !snapshot.requireData.expanded,
builder: (context, snapshot) { );
return AnimatedRotation( }
duration: Duration(milliseconds: 300), },
turns: (snapshot.data?.expanded ?? false)
? 0.5
: 0.0,
child: _Button(
'down.png',
() async {
if (snapshot.hasData) {
_cardsViewController.setExpanded(
!snapshot.requireData.expanded,
);
}
},
),
);
}),
_Button('shuffle.png', () async {
final cards =
_cardsViewController.state.packCards;
if (cards.isEmpty) return;
final index = (_cardsViewController.offsetCards +
Random().nextInt(
(cards.length / 2).ceil(),
) +
3) %
cards.length;
await _cardsViewController.animateTo(index);
AutoRouter.of(context).push(
PageRouteInfo(
CardGamePage.name,
args: CardGamePageArgs(
cards: cards,
color: cardPack.color?.asColor,
startCardId: cards[index].id,
infinite: true,
shuffle: true,
// id: cardPack.dto.id.toString(),
), ),
);
}),
_Button('shuffle.png', () async {
final cards = _cardsViewController.state.packCards;
if (cards.isEmpty) return;
final index = (_cardsViewController.offsetCards +
Random().nextInt(
(cards.length / 2).ceil(),
) +
3) %
cards.length;
await _cardsViewController.animateTo(index);
AutoRouter.of(context).push(
PageRouteInfo(
CardGamePage.name,
args: CardGamePageArgs(
cards: cards,
color: cardPack.color?.asColor,
startCardId: cards[index].id,
infinite: true,
shuffle: true,
// id: cardPack.dto.id.toString(),
), ),
); ),
}), );
// _Button('chat.png', () {}), }),
StreamBuilder( // _Button('chat.png', () {}),
stream: _cardsViewController.asStream, StreamBuilder(
builder: (context, snapshot) { stream: _cardsViewController.asStream,
return _Button( builder: (context, snapshot) {
(snapshot.data?.favorite ?? false) return _Button(
? 'heart_fill.png' (snapshot.data?.favorite ?? false)
: 'heart.png', () { ? 'heart_fill.png'
if (snapshot.hasData) { : 'heart.png', () {
_cardsViewController.setFavorite( if (snapshot.hasData) {
!snapshot.requireData.favorite, _cardsViewController.setFavorite(
); !snapshot.requireData.favorite,
} );
}); }
}), });
], }),
), ],
), ),
], ),
); ],
}), );
_AdTile(), }),
_TestsSection(cardPack.id.toString()), _AdTile(),
// _TestButton( _TestsSection(cardPack.id.toString()),
// title: 'Быстрый тест', // _TestButton(
// subtitle: 'Лучший результат: 99 / 100', // title: 'Быстрый тест',
// time: '3', // subtitle: 'Лучший результат: 99 / 100',
// timeSubtitle: 'мин', // time: '3',
// onTap: () {}, // timeSubtitle: 'мин',
// ), // onTap: () {},
// _TestButton( // ),
// title: 'Полный тест', // _TestButton(
// subtitle: 'Лучший результат: 99 / 100', // title: 'Полный тест',
// time: '10', // subtitle: 'Лучший результат: 99 / 100',
// timeSubtitle: 'мин', // time: '10',
// onTap: () {}, // timeSubtitle: 'мин',
// ), // onTap: () {},
], // ),
), ],
), ),
), ),
Hero( Hero(