Audio section widget (#744)
first draft of word focus listening activities using text to speech librarypull/1428/head
parent
925d7506ef
commit
ac80e6217c
@ -1,13 +1,6 @@
|
||||
enum ActivityDisplayInstructionsEnum { highlight, hide }
|
||||
enum ActivityDisplayInstructionsEnum { highlight, hide, nothing }
|
||||
|
||||
extension ActivityDisplayInstructionsEnumExt
|
||||
on ActivityDisplayInstructionsEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityDisplayInstructionsEnum.highlight:
|
||||
return 'highlight';
|
||||
case ActivityDisplayInstructionsEnum.hide:
|
||||
return 'hide';
|
||||
}
|
||||
}
|
||||
String get string => toString().split('.').last;
|
||||
}
|
||||
|
||||
@ -1,195 +1,195 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../enum/vocab_proficiency_enum.dart';
|
||||
|
||||
class VocabHeadwords {
|
||||
List<VocabList> lists;
|
||||
|
||||
VocabHeadwords({
|
||||
required this.lists,
|
||||
});
|
||||
|
||||
/// in json parameter, keys are the names of the VocabList
|
||||
/// values are the words in the VocabList
|
||||
factory VocabHeadwords.fromJson(Map<String, dynamic> json) {
|
||||
final List<VocabList> lists = [];
|
||||
for (final entry in json.entries) {
|
||||
lists.add(
|
||||
VocabList(
|
||||
name: entry.key,
|
||||
lemmas: (entry.value as Iterable).cast<String>().toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return VocabHeadwords(lists: lists);
|
||||
}
|
||||
|
||||
static Future<VocabHeadwords> getHeadwords(String langCode) async {
|
||||
final String data =
|
||||
await rootBundle.loadString('${langCode}_headwords.json');
|
||||
final decoded = jsonDecode(data);
|
||||
final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded);
|
||||
return headwords;
|
||||
}
|
||||
}
|
||||
|
||||
class VocabList {
|
||||
String name;
|
||||
|
||||
/// key is lemma
|
||||
Map<String, VocabTotals> words = {};
|
||||
|
||||
VocabList({
|
||||
required this.name,
|
||||
required List<String> lemmas,
|
||||
}) {
|
||||
for (final lemma in lemmas) {
|
||||
words[lemma] = VocabTotals.newTotals;
|
||||
}
|
||||
}
|
||||
|
||||
void addVocabUse(String lemma, List<OneConstructUse> use) {
|
||||
words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use);
|
||||
}
|
||||
|
||||
ListTotals calculuateTotals() {
|
||||
final ListTotals listTotals = ListTotals.empty;
|
||||
for (final word in words.entries) {
|
||||
debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase());
|
||||
listTotals.addByType(word.value.proficiencyLevel);
|
||||
}
|
||||
return listTotals;
|
||||
}
|
||||
}
|
||||
|
||||
class ListTotals {
|
||||
int low;
|
||||
int medium;
|
||||
int high;
|
||||
int unknown;
|
||||
|
||||
ListTotals({
|
||||
required this.low,
|
||||
required this.medium,
|
||||
required this.high,
|
||||
required this.unknown,
|
||||
});
|
||||
|
||||
static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0);
|
||||
|
||||
void addByType(VocabProficiencyEnum prof) {
|
||||
switch (prof) {
|
||||
case VocabProficiencyEnum.low:
|
||||
low++;
|
||||
break;
|
||||
case VocabProficiencyEnum.medium:
|
||||
medium++;
|
||||
break;
|
||||
case VocabProficiencyEnum.high:
|
||||
high++;
|
||||
break;
|
||||
case VocabProficiencyEnum.unk:
|
||||
unknown++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VocabTotals {
|
||||
num ga;
|
||||
|
||||
num wa;
|
||||
|
||||
num corIt;
|
||||
|
||||
num incIt;
|
||||
|
||||
num ignIt;
|
||||
|
||||
VocabTotals({
|
||||
required this.ga,
|
||||
required this.wa,
|
||||
required this.corIt,
|
||||
required this.incIt,
|
||||
required this.ignIt,
|
||||
});
|
||||
|
||||
num get calculateEstimatedVocabProficiency {
|
||||
const num gaWeight = -1;
|
||||
const num waWeight = 1;
|
||||
const num corItWeight = 0.5;
|
||||
const num incItWeight = -0.5;
|
||||
const num ignItWeight = 0.1;
|
||||
|
||||
final num gaScore = ga * gaWeight;
|
||||
final num waScore = wa * waWeight;
|
||||
final num corItScore = corIt * corItWeight;
|
||||
final num incItScore = incIt * incItWeight;
|
||||
final num ignItScore = ignIt * ignItWeight;
|
||||
|
||||
final num totalScore =
|
||||
gaScore + waScore + corItScore + incItScore + ignItScore;
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
VocabProficiencyEnum get proficiencyLevel =>
|
||||
VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency);
|
||||
|
||||
static VocabTotals get newTotals {
|
||||
return VocabTotals(
|
||||
ga: 0,
|
||||
wa: 0,
|
||||
corIt: 0,
|
||||
incIt: 0,
|
||||
ignIt: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
switch (use.useType) {
|
||||
case ConstructUseTypeEnum.ga:
|
||||
ga++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.wa:
|
||||
wa++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
ignIt++;
|
||||
break;
|
||||
//TODO - these shouldn't be counted as such
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
ignIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
//TODO if we bring back Headwords then we need to add these
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
break;
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// import 'dart:convert';
|
||||
// import 'dart:developer';
|
||||
|
||||
// import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
|
||||
// import '../enum/vocab_proficiency_enum.dart';
|
||||
|
||||
// class VocabHeadwords {
|
||||
// List<VocabList> lists;
|
||||
|
||||
// VocabHeadwords({
|
||||
// required this.lists,
|
||||
// });
|
||||
|
||||
// /// in json parameter, keys are the names of the VocabList
|
||||
// /// values are the words in the VocabList
|
||||
// factory VocabHeadwords.fromJson(Map<String, dynamic> json) {
|
||||
// final List<VocabList> lists = [];
|
||||
// for (final entry in json.entries) {
|
||||
// lists.add(
|
||||
// VocabList(
|
||||
// name: entry.key,
|
||||
// lemmas: (entry.value as Iterable).cast<String>().toList(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// return VocabHeadwords(lists: lists);
|
||||
// }
|
||||
|
||||
// static Future<VocabHeadwords> getHeadwords(String langCode) async {
|
||||
// final String data =
|
||||
// await rootBundle.loadString('${langCode}_headwords.json');
|
||||
// final decoded = jsonDecode(data);
|
||||
// final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded);
|
||||
// return headwords;
|
||||
// }
|
||||
// }
|
||||
|
||||
// class VocabList {
|
||||
// String name;
|
||||
|
||||
// /// key is lemma
|
||||
// Map<String, VocabTotals> words = {};
|
||||
|
||||
// VocabList({
|
||||
// required this.name,
|
||||
// required List<String> lemmas,
|
||||
// }) {
|
||||
// for (final lemma in lemmas) {
|
||||
// words[lemma] = VocabTotals.newTotals;
|
||||
// }
|
||||
// }
|
||||
|
||||
// void addVocabUse(String lemma, List<OneConstructUse> use) {
|
||||
// words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use);
|
||||
// }
|
||||
|
||||
// ListTotals calculuateTotals() {
|
||||
// final ListTotals listTotals = ListTotals.empty;
|
||||
// for (final word in words.entries) {
|
||||
// debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase());
|
||||
// listTotals.addByType(word.value.proficiencyLevel);
|
||||
// }
|
||||
// return listTotals;
|
||||
// }
|
||||
// }
|
||||
|
||||
// class ListTotals {
|
||||
// int low;
|
||||
// int medium;
|
||||
// int high;
|
||||
// int unknown;
|
||||
|
||||
// ListTotals({
|
||||
// required this.low,
|
||||
// required this.medium,
|
||||
// required this.high,
|
||||
// required this.unknown,
|
||||
// });
|
||||
|
||||
// static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0);
|
||||
|
||||
// void addByType(VocabProficiencyEnum prof) {
|
||||
// switch (prof) {
|
||||
// case VocabProficiencyEnum.low:
|
||||
// low++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.medium:
|
||||
// medium++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.high:
|
||||
// high++;
|
||||
// break;
|
||||
// case VocabProficiencyEnum.unk:
|
||||
// unknown++;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// class VocabTotals {
|
||||
// num ga;
|
||||
|
||||
// num wa;
|
||||
|
||||
// num corIt;
|
||||
|
||||
// num incIt;
|
||||
|
||||
// num ignIt;
|
||||
|
||||
// VocabTotals({
|
||||
// required this.ga,
|
||||
// required this.wa,
|
||||
// required this.corIt,
|
||||
// required this.incIt,
|
||||
// required this.ignIt,
|
||||
// });
|
||||
|
||||
// num get calculateEstimatedVocabProficiency {
|
||||
// const num gaWeight = -1;
|
||||
// const num waWeight = 1;
|
||||
// const num corItWeight = 0.5;
|
||||
// const num incItWeight = -0.5;
|
||||
// const num ignItWeight = 0.1;
|
||||
|
||||
// final num gaScore = ga * gaWeight;
|
||||
// final num waScore = wa * waWeight;
|
||||
// final num corItScore = corIt * corItWeight;
|
||||
// final num incItScore = incIt * incItWeight;
|
||||
// final num ignItScore = ignIt * ignItWeight;
|
||||
|
||||
// final num totalScore =
|
||||
// gaScore + waScore + corItScore + incItScore + ignItScore;
|
||||
|
||||
// return totalScore;
|
||||
// }
|
||||
|
||||
// VocabProficiencyEnum get proficiencyLevel =>
|
||||
// VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency);
|
||||
|
||||
// static VocabTotals get newTotals {
|
||||
// return VocabTotals(
|
||||
// ga: 0,
|
||||
// wa: 0,
|
||||
// corIt: 0,
|
||||
// incIt: 0,
|
||||
// ignIt: 0,
|
||||
// );
|
||||
// }
|
||||
|
||||
// void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// switch (use.useType) {
|
||||
// case ConstructUseTypeEnum.ga:
|
||||
// ga++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.wa:
|
||||
// wa++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.corIt:
|
||||
// corIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incIt:
|
||||
// incIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.ignIt:
|
||||
// ignIt++;
|
||||
// break;
|
||||
// //TODO - these shouldn't be counted as such
|
||||
// case ConstructUseTypeEnum.ignIGC:
|
||||
// ignIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.corIGC:
|
||||
// corIt++;
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incIGC:
|
||||
// incIt++;
|
||||
// break;
|
||||
// //TODO if we bring back Headwords then we need to add these
|
||||
// case ConstructUseTypeEnum.corPA:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.incPA:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.unk:
|
||||
// break;
|
||||
// case ConstructUseTypeEnum.ignPA:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
||||
class MissingVoiceButton extends StatelessWidget {
|
||||
final String targetLangCode;
|
||||
|
||||
const MissingVoiceButton({
|
||||
required this.targetLangCode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
void launchTTSSettings(BuildContext context) {
|
||||
if (Platform.isAndroid) {
|
||||
const intent = AndroidIntent(
|
||||
action: 'com.android.settings.TTS_SETTINGS',
|
||||
package: 'com.talktolearn.chat',
|
||||
);
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: intent.launch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.voiceNotAvailable,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchTTSSettings,
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(L10n.of(context)!.openVoiceSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart' as flutter_tts;
|
||||
|
||||
class TtsController {
|
||||
String? targetLanguage;
|
||||
|
||||
List<String> availableLangCodes = [];
|
||||
final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts();
|
||||
|
||||
// if targetLanguage isn't set here, it needs to be set later
|
||||
TtsController() {
|
||||
setupTTS();
|
||||
}
|
||||
|
||||
Future<void> setupTTS() async {
|
||||
try {
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
debugger(when: kDebugMode && targetLanguage == null);
|
||||
|
||||
debugPrint('setupTTS targetLanguage: $targetLanguage');
|
||||
|
||||
tts.setLanguage(
|
||||
targetLanguage ?? "en",
|
||||
);
|
||||
|
||||
await tts.awaitSpeakCompletion(true);
|
||||
|
||||
final voices = await tts.getVoices;
|
||||
availableLangCodes = (voices as List)
|
||||
.map((v) {
|
||||
// debugPrint('v: $v');
|
||||
|
||||
//@ggurdin i changed this from name to locale
|
||||
//in my testing, that's where the language code is stored
|
||||
// maybe it's different for different devices? was it different in your android testing?
|
||||
// return v['name']?.split("-").first;
|
||||
return v['locale']?.split("-").first;
|
||||
})
|
||||
.toSet()
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
debugPrint("lang supported? $isLanguageFullySupported");
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> speak(String text) async {
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
await tts.stop();
|
||||
return tts.speak(text);
|
||||
}
|
||||
|
||||
bool get isLanguageFullySupported =>
|
||||
availableLangCodes.contains(targetLanguage);
|
||||
|
||||
// @ggurdin
|
||||
Widget get missingVoiceButton => targetLanguage != null &&
|
||||
(kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid)
|
||||
? const SizedBox.shrink()
|
||||
: MissingVoiceButton(
|
||||
targetLangCode: targetLanguage!,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class WordAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
|
||||
const WordAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
WordAudioButtonState createState() => WordAudioButtonState();
|
||||
}
|
||||
|
||||
class WordAudioButtonState extends State<WordAudioButton> {
|
||||
bool _isPlaying = false;
|
||||
|
||||
TtsController ttsController = TtsController();
|
||||
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
ttsController.setupTTS().then((value) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_arrow_outlined),
|
||||
isSelected: _isPlaying,
|
||||
selectedIcon: const Icon(Icons.pause_outlined),
|
||||
color: _isPlaying ? Colors.white : null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
_isPlaying
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
tooltip:
|
||||
_isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio,
|
||||
onPressed: () async {
|
||||
if (_isPlaying) {
|
||||
await ttsController.tts.stop();
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
});
|
||||
await ttsController.speak(widget.text);
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
});
|
||||
}
|
||||
}, // Disable button if language isn't supported
|
||||
),
|
||||
ttsController.missingVoiceButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordFocusListeningActivity extends StatefulWidget {
|
||||
final PracticeActivityModel activity;
|
||||
final MessagePracticeActivityCardState practiceCardController;
|
||||
|
||||
const WordFocusListeningActivity({
|
||||
super.key,
|
||||
required this.activity,
|
||||
required this.practiceCardController,
|
||||
});
|
||||
|
||||
@override
|
||||
WordFocusListeningActivityState createState() =>
|
||||
WordFocusListeningActivityState();
|
||||
|
||||
ActivityContent get activityContent => activity.content;
|
||||
}
|
||||
|
||||
class WordFocusListeningActivityState
|
||||
extends State<WordFocusListeningActivity> {
|
||||
int? selectedChoiceIndex;
|
||||
|
||||
TtsController tts = TtsController();
|
||||
|
||||
final double buttonSize = 40;
|
||||
|
||||
PracticeActivityRecordModel? get currentRecordModel =>
|
||||
widget.practiceCardController.currentCompletionRecord;
|
||||
|
||||
initializeTTS() async {
|
||||
tts.setupTTS().then((value) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializeTTS();
|
||||
}
|
||||
|
||||
void checkAnswer(int index) {
|
||||
final String value = widget.activityContent.choices[index];
|
||||
|
||||
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool isCorrect = widget.activity.content.isCorrect(value, index);
|
||||
|
||||
currentRecordModel?.addResponse(
|
||||
text: value,
|
||||
score: isCorrect ? 1 : 0,
|
||||
);
|
||||
|
||||
if (currentRecordModel == null ||
|
||||
currentRecordModel!.latestResponse == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id,
|
||||
constructs: currentRecordModel!.latestResponse!.toUses(
|
||||
widget.practiceCardController.currentActivity!,
|
||||
widget.practiceCardController.metadata,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
selectedChoiceIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
// Text question at the top
|
||||
Text(
|
||||
widget.activityContent.question,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Blank slot for the answer
|
||||
DragTarget<int>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return CircleAvatar(
|
||||
radius: buttonSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppConfig.primaryColor.withOpacity(0.4),
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAcceptWithDetails: (details) => checkAnswer(details.data),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Audio options as draggable buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(
|
||||
widget.activityContent.choices.length,
|
||||
(index) => Draggable<int>(
|
||||
data: index,
|
||||
feedback: _buildAudioButton(context, theme, index),
|
||||
childWhenDragging: _buildAudioButton(context, theme, index, true),
|
||||
child: _buildAudioButton(context, theme, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to build the audio buttons
|
||||
Widget _buildAudioButton(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
int index, [
|
||||
bool dragging = false,
|
||||
]) {
|
||||
final isAnswerCorrect = widget.activityContent.isCorrect(
|
||||
widget.activityContent.choices[index],
|
||||
index,
|
||||
);
|
||||
Color buttonColor;
|
||||
if (selectedChoiceIndex == index) {
|
||||
buttonColor = isAnswerCorrect
|
||||
? theme.colorScheme.secondary.withOpacity(0.7) // Correct: Green
|
||||
: theme.colorScheme.error.withOpacity(0.7); // Incorrect: Red
|
||||
} else {
|
||||
buttonColor =
|
||||
AppConfig.primaryColor.withOpacity(0.4); // Default: Primary color
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => tts.speak(widget.activityContent.choices[index]),
|
||||
child: CircleAvatar(
|
||||
radius: buttonSize,
|
||||
backgroundColor: dragging ? Colors.grey.withOpacity(0.5) : buttonColor,
|
||||
child: const Icon(Icons.play_arrow),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue