audio
This commit is contained in:
parent
6919ef1e6c
commit
69afc51781
7 changed files with 259 additions and 203 deletions
Binary file not shown.
117
lib/features/tests/test_widgets/common_test_question_widget.dart
Normal file
117
lib/features/tests/test_widgets/common_test_question_widget.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue