diff --git a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk index 7926d3a..97d5376 100644 Binary files a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk and b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk differ diff --git a/lib/features/tests/test_widgets/common_test_question_widget.dart b/lib/features/tests/test_widgets/common_test_question_widget.dart new file mode 100644 index 0000000..f9d0add --- /dev/null +++ b/lib/features/tests/test_widgets/common_test_question_widget.dart @@ -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 _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); + } + } +} diff --git a/lib/features/tests/test_widgets/input_buttons_test.dart b/lib/features/tests/test_widgets/input_buttons_test.dart index 7d72586..7569124 100644 --- a/lib/features/tests/test_widgets/input_buttons_test.dart +++ b/lib/features/tests/test_widgets/input_buttons_test.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.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/theme/themes.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; @@ -47,44 +48,13 @@ class InputButtonsTestWidget extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (model.image != null) - Container( - margin: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.white, - 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, - ), - ), + Expanded( + child: CommonTestQuestionWidget( + text: model.text, + image: model.image, + audio: model.audio, ), + ), StatefulBuilder(builder: (context, setState) { void buttonTapped(int index, TestButtonDto button) { if (state.answer.fold(0, (p, n) => p + n.length) >= @@ -118,7 +88,7 @@ class InputButtonsTestWidget extends StatelessWidget { } 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( ' ', (model.template.length / 2).ceil(), diff --git a/lib/features/tests/test_widgets/simple_test.dart b/lib/features/tests/test_widgets/simple_test.dart index dc5476c..2b218c0 100644 --- a/lib/features/tests/test_widgets/simple_test.dart +++ b/lib/features/tests/test_widgets/simple_test.dart @@ -2,8 +2,11 @@ import 'dart:math'; import 'package:flutter/material.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_widgets/common_test_question_widget.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 '../../../di/locator.dart'; @@ -28,7 +31,6 @@ class SimpleTestWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context).textTheme; final random = Random(state.seed); final buttons = [...model.buttons]..shuffle(random); return LayoutBuilder(builder: (context, constraints) { @@ -37,34 +39,13 @@ class SimpleTestWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (model.image != null) - Flexible( - flex: 4, - child: Container( - margin: const EdgeInsets.all(4.0), - 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, - ), - ), - ), + Expanded( + child: CommonTestQuestionWidget( + text: model.text, + image: model.image, + audio: model.audio, ), + ), SizedBox( height: 16.0, ), diff --git a/lib/managers/repository/http_repository.dart b/lib/managers/repository/http_repository.dart index 43f2487..685cc45 100644 --- a/lib/managers/repository/http_repository.dart +++ b/lib/managers/repository/http_repository.dart @@ -50,20 +50,24 @@ class HttpRepository extends Repository with Api { ExternalIdType idType, String? name, ) async { - final r = await _dio.post('$path/user/create', data: { - 'token': externalId, - 'tokenType': idType.name, - 'name': name, - }).catchError( - (error, stackTrace) => log('', error: error, stackTrace: stackTrace)); - //todo add client header encryption ?? - final authToken = r.headers[HttpHeaders.authorizationHeader]!.last; - return ( + try { + final r = await _dio.post('$path/user/create', data: { + 'token': externalId, + 'tokenType': idType.name, + 'name': name, + }); + //todo add client header encryption ?? + final authToken = r.headers[HttpHeaders.authorizationHeader]!.last; + return ( UserDto.fromJson( jsonDecode(r.data as String) as Map, ), authToken, - ); + ); + } on DioException catch (e, s) { + log('User create error', error: e, stackTrace: s); + rethrow; + } } void setAuthToken(String? authToken) { diff --git a/lib/managers/user_manager.dart b/lib/managers/user_manager.dart index 2a9c01e..4751e72 100644 --- a/lib/managers/user_manager.dart +++ b/lib/managers/user_manager.dart @@ -59,6 +59,7 @@ class UserManager { ExternalIdType idType, String? name, ) async { + await locator.packCacheManager.clearCache(); final (user, token) = await _repository.createOrGetUser(externalId, idType, name); _setAuthToken(token); diff --git a/lib/widgets/card_pack/available_pack.dart b/lib/widgets/card_pack/available_pack.dart index 670a8af..b37d12e 100644 --- a/lib/widgets/card_pack/available_pack.dart +++ b/lib/widgets/card_pack/available_pack.dart @@ -33,139 +33,122 @@ class AvailablePack extends StatelessWidget { Analytics.availablePackOpened(cardPack); return Column( children: [ - CardPackHeader( - cardPack.title, - cardPack.subtitle, - Colors.transparent, - ), + CardPackHeader.fromDto(cardPack), Expanded( - child: RefreshIndicator( - onRefresh: () => - locator.packUpdater.updatePackAndSaveImages(cardPack.id), - backgroundColor: Colors.white, - color: cardPack.color?.asColor ?? Colors.black, - child: ListView( - physics: BouncingScrollPhysics( - decelerationRate: ScrollDecelerationRate.fast, - parent: AlwaysScrollableScrollPhysics()), - controller: _mainScrollController, - children: [ - StatefulBuilder(builder: (context, setState) { - return Column( - children: [ - Container( - height: 1, - color: cardPack.color?.asColor ?? - Colors.grey.withOpacity(0.5), + child: ListView( + physics: BouncingScrollPhysics( + decelerationRate: ScrollDecelerationRate.fast, + parent: AlwaysScrollableScrollPhysics()), + controller: _mainScrollController, + children: [ + StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + color: cardPack.color?.asColor?.withOpacity(0.1), ), - Container( - decoration: BoxDecoration( - color: cardPack.color?.asColor?.withOpacity(0.1), - ), - alignment: Alignment.center, - clipBehavior: Clip.antiAlias, - child: CardsView(_cardsViewController, cardPack), - + alignment: Alignment.center, + clipBehavior: Clip.antiAlias, + child: CardsView(_cardsViewController, cardPack), + ), + Container( + height: 1, + color: cardPack.color?.asColor ?? + Colors.grey.withOpacity(0.5), + ), + SizedBox( + height: 10.h, + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 10.w, ), - Container( - height: 1, - color: cardPack.color?.asColor ?? - Colors.grey.withOpacity(0.5), - ), - SizedBox( - height: 10.h, - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: 10.w, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamBuilder( - stream: _cardsViewController.asStream, - 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(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamBuilder( + stream: _cardsViewController.asStream, + 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('chat.png', () {}), - StreamBuilder( - stream: _cardsViewController.asStream, - builder: (context, snapshot) { - return _Button( - (snapshot.data?.favorite ?? false) - ? 'heart_fill.png' - : 'heart.png', () { - if (snapshot.hasData) { - _cardsViewController.setFavorite( - !snapshot.requireData.favorite, - ); - } - }); - }), - ], - ), + ), + ); + }), + // _Button('chat.png', () {}), + StreamBuilder( + stream: _cardsViewController.asStream, + builder: (context, snapshot) { + return _Button( + (snapshot.data?.favorite ?? false) + ? 'heart_fill.png' + : 'heart.png', () { + if (snapshot.hasData) { + _cardsViewController.setFavorite( + !snapshot.requireData.favorite, + ); + } + }); + }), + ], ), - ], - ); - }), - _AdTile(), - _TestsSection(cardPack.id.toString()), - // _TestButton( - // title: 'Быстрый тест', - // subtitle: 'Лучший результат: 99 / 100', - // time: '3', - // timeSubtitle: 'мин', - // onTap: () {}, - // ), - // _TestButton( - // title: 'Полный тест', - // subtitle: 'Лучший результат: 99 / 100', - // time: '10', - // timeSubtitle: 'мин', - // onTap: () {}, - // ), - ], - ), + ), + ], + ); + }), + _AdTile(), + _TestsSection(cardPack.id.toString()), + // _TestButton( + // title: 'Быстрый тест', + // subtitle: 'Лучший результат: 99 / 100', + // time: '3', + // timeSubtitle: 'мин', + // onTap: () {}, + // ), + // _TestButton( + // title: 'Полный тест', + // subtitle: 'Лучший результат: 99 / 100', + // time: '10', + // timeSubtitle: 'мин', + // onTap: () {}, + // ), + ], ), ), Hero(