diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2eb411a23..68c90c59e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -158,4 +158,10 @@
android:name="flutterEmbedding"
android:value="2" />
+
+
+
+
+
+
diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb
index 83de9e423..4e709a9c2 100644
--- a/assets/l10n/intl_en.arb
+++ b/assets/l10n/intl_en.arb
@@ -4239,6 +4239,10 @@
"l2SupportAlpha": "Alpha",
"l2SupportBeta": "Beta",
"l2SupportFull": "Full",
+ "voiceNotAvailable": "It looks like you don't have a voice installed for this language.",
+ "openVoiceSettings": "Click here to open voice settings",
+ "playAudio": "Play",
+ "stop": "Stop",
"grammarCopySCONJ": "Subordinating Conjunction",
"grammarCopyNUM": "Number",
"grammarCopyVERB": "Verb",
diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart
index 3213d085f..66417d921 100644
--- a/lib/pages/chat/events/audio_player.dart
+++ b/lib/pages/chat/events/audio_player.dart
@@ -25,7 +25,13 @@ class AudioPlayerWidget extends StatefulWidget {
static String? currentId;
- static const int wavesCount = 40;
+ // #Pangea
+ // static const int wavesCount = 40;
+ static const int wavesCount = kIsWeb ? 100 : 40;
+
+ final int? sectionStartMS;
+ final int? sectionEndMS;
+ // Pangea#
const AudioPlayerWidget(
this.event, {
@@ -33,6 +39,8 @@ class AudioPlayerWidget extends StatefulWidget {
// #Pangea
this.matrixFile,
this.autoplay = false,
+ this.sectionStartMS,
+ this.sectionEndMS,
// Pangea#
super.key,
});
@@ -72,6 +80,24 @@ class AudioPlayerState extends State {
super.dispose();
}
+ // #Pangea
+ // @override
+ // void didUpdateWidget(covariant oldWidget) {
+ // if ((oldWidget.sectionEndMS != widget.sectionEndMS) ||
+ // (oldWidget.sectionStartMS != widget.sectionStartMS)) {
+ // debugPrint('selection changed');
+ // if (widget.sectionStartMS != null) {
+ // audioPlayer?.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // audioPlayer?.play();
+ // } else {
+ // audioPlayer?.stop();
+ // audioPlayer?.seek(null);
+ // }
+ // }
+ // super.didUpdateWidget(oldWidget);
+ // }
+ // Pangea#
+
Future _downloadAction() async {
// #Pangea
// if (status != AudioPlayerStatus.notDownloaded) return;
@@ -160,7 +186,16 @@ class AudioPlayerState extends State {
AudioPlayerWidget.wavesCount)
.round();
});
+ // #Pangea
+ // if (widget.sectionStartMS != null &&
+ // widget.sectionEndMS != null &&
+ // state.inMilliseconds.toDouble() >= widget.sectionEndMS!) {
+ // audioPlayer.stop();
+ // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // } else
if (state.inMilliseconds.toDouble() == maxPosition) {
+ // if (state.inMilliseconds.toDouble() == maxPosition) {
+ // Pangea#
audioPlayer.stop();
audioPlayer.seek(null);
}
@@ -194,6 +229,11 @@ class AudioPlayerState extends State {
}
// Pangea#
}
+ // #Pangea
+ // if (widget.sectionStartMS != null) {
+ // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // }
+ // Pangea#
audioPlayer.play().onError(
ErrorReporter(context, 'Unable to play audio message')
.onErrorCallback,
@@ -311,6 +351,17 @@ class AudioPlayerState extends State {
final statusText = this.statusText ??= _durationString ?? '00:00';
final audioPlayer = this.audioPlayer;
+
+ // #Pangea
+ final msPerWave = (maxPosition / AudioPlayerWidget.wavesCount);
+ final int? startWave = widget.sectionStartMS != null && msPerWave > 0
+ ? (widget.sectionStartMS! / msPerWave).floor()
+ : null;
+ final int? endWave = widget.sectionEndMS != null && msPerWave > 0
+ ? (widget.sectionEndMS! / msPerWave).ceil()
+ : null;
+ // Pangea#
+
return Padding(
// #Pangea
// padding: const EdgeInsets.all(12.0),
@@ -352,44 +403,101 @@ class AudioPlayerState extends State {
// #Pangea
// const SizedBox(width: 8),
const SizedBox(width: 5),
- // Pangea#
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
- GestureDetector(
- onTapDown: (_) => audioPlayer?.seek(
- Duration(
- milliseconds:
- (maxPosition / AudioPlayerWidget.wavesCount).round() *
- i,
- ),
- ),
- child: Container(
- height: 32,
- color: widget.color.withAlpha(0),
- alignment: Alignment.center,
- child: Opacity(
- opacity: currentPosition > i ? 1 : 0.5,
- child: Container(
- margin: const EdgeInsets.symmetric(horizontal: 1),
- decoration: BoxDecoration(
- color: widget.color,
- borderRadius: BorderRadius.circular(2),
- ),
- // #Pangea
- // width: 2,
- width: 1,
- // Pangea#
- height: 32 * (waveform[i] / 1024),
- ),
- ),
- ),
- ),
- ],
- ),
- // #Pangea
+ // Row(
+ // mainAxisSize: MainAxisSize.min,
+ // children: [
+ // for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
+ // GestureDetector(
+ // onTapDown: (_) => audioPlayer?.seek(
+ // Duration(
+ // milliseconds:
+ // (maxPosition / AudioPlayerWidget.wavesCount).round() *
+ // i,
+ // ),
+ // ),
+ // child: Container(
+ // height: 32,
+ // color: widget.color.withAlpha(0),
+ // alignment: Alignment.center,
+ // child: Opacity(
+ // opacity: currentPosition > i ? 1 : 0.5,
+ // child: Container(
+ // margin: const EdgeInsets.symmetric(horizontal: 1),
+ // decoration: BoxDecoration(
+ // color: widget.color,
+ // borderRadius: BorderRadius.circular(2),
+ // ),
+ // // #Pangea
+ // // width: 2,
+ // width: 1,
+ // // Pangea#
+ // height: 32 * (waveform[i] / 1024),
+ // ),
+ // ),
+ // ),
+ // ),
+ // ],
+ // ),
// const SizedBox(width: 8),
+ Expanded(
+ child: Row(
+ children: [
+ for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
+ Builder(
+ builder: (context) {
+ final double barOpacity = currentPosition > i ? 1 : 0.5;
+ return Expanded(
+ child: GestureDetector(
+ onTapDown: (_) {
+ audioPlayer?.seek(
+ Duration(
+ milliseconds:
+ (maxPosition / AudioPlayerWidget.wavesCount)
+ .round() *
+ i,
+ ),
+ );
+ },
+ child: Stack(
+ children: [
+ Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 0.5,
+ ),
+ decoration: BoxDecoration(
+ color: widget.color.withOpacity(barOpacity),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ height: 32 * (waveform[i] / 1024),
+ ),
+ ],
+ ),
+ ),
+ );
+ // return Container(
+ // height: 32,
+ // width: 2,
+ // alignment: Alignment.center,
+ // child: Opacity(
+ // opacity: barOpacity,
+ // child: Container(
+ // margin: const EdgeInsets.symmetric(
+ // horizontal: 1,
+ // ),
+ // decoration: BoxDecoration(
+ // color: widget.color,
+ // borderRadius: BorderRadius.circular(2),
+ // ),
+ // height: 32 * (waveform[i] / 1024),
+ // width: 2,
+ // ),
+ // ),
+ // );
+ },
+ ),
+ ],
+ ),
+ ),
const SizedBox(width: 5),
// SizedBox(
// width: 36,
diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart
index 1d3c7f7ae..a8d7cca36 100644
--- a/lib/pangea/controllers/practice_activity_generation_controller.dart
+++ b/lib/pangea/controllers/practice_activity_generation_controller.dart
@@ -162,7 +162,7 @@ class PracticeGenerationController {
activityType: ActivityTypeEnum.multipleChoice,
langCode: event.messageDisplayLangCode,
msgId: event.eventId,
- multipleChoice: MultipleChoice(
+ content: ActivityContent(
question: "What is a synonym for 'happy'?",
choices: ["sad", "angry", "joyful", "tired"],
answer: "joyful",
diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart
index 069722590..e032c4045 100644
--- a/lib/pangea/controllers/text_to_speech_controller.dart
+++ b/lib/pangea/controllers/text_to_speech_controller.dart
@@ -5,20 +5,93 @@ import 'dart:typed_data';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
+import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:http/http.dart';
import '../network/requests.dart';
-class TextToSpeechRequest {
- String text;
- String langCode;
+class PangeaAudioEventData {
+ final String text;
+ final String langCode;
+ final List tokens;
- TextToSpeechRequest({required this.text, required this.langCode});
+ PangeaAudioEventData({
+ required this.text,
+ required this.langCode,
+ required this.tokens,
+ });
+
+ factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData(
+ text: json[ModelKey.text] as String,
+ langCode: json[ModelKey.langCode] as String,
+ tokens: List.from(
+ (json[ModelKey.tokens] as Iterable)
+ .map((x) => TTSToken.fromJson(x))
+ .toList(),
+ ),
+ );
Map toJson() => {
ModelKey.text: text,
ModelKey.langCode: langCode,
+ ModelKey.tokens:
+ List