merge main into move-server-main
commit
0b8862751d
@ -1,18 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
class BaseController<T> {
|
||||
final StreamController<T> stateListener = StreamController<T>();
|
||||
final StreamController<T> _stateListener = StreamController<T>();
|
||||
late Stream<T> stateStream;
|
||||
|
||||
BaseController() {
|
||||
stateStream = stateListener.stream.asBroadcastStream();
|
||||
stateStream = _stateListener.stream.asBroadcastStream();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
stateListener.close();
|
||||
_stateListener.close();
|
||||
}
|
||||
|
||||
setState(T data) {
|
||||
stateListener.add(data);
|
||||
_stateListener.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,16 +1,12 @@
|
||||
enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking }
|
||||
enum ActivityTypeEnum { multipleChoice, wordFocusListening }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return 'free_response';
|
||||
case ActivityTypeEnum.listening:
|
||||
return 'listening';
|
||||
case ActivityTypeEnum.speaking:
|
||||
return 'speaking';
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return 'word_focus_listening';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,72 @@
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
|
||||
import 'pangea_token_model.dart';
|
||||
|
||||
class TokensRequestModel {
|
||||
/// the text to be tokenized
|
||||
String fullText;
|
||||
|
||||
/// if known, [langCode] is the language of of the text
|
||||
/// it is used to determine which model to use in tokenizing
|
||||
String? langCode;
|
||||
|
||||
/// [senderL1] and [senderL2] are the languages of the sender
|
||||
/// if langCode is not known, the [senderL1] and [senderL2] will be used to help determine the language of the text
|
||||
/// if langCode is known, [senderL1] and [senderL2] will be used to determine whether the tokens need
|
||||
/// pos/mporph tags and whether lemmas are eligible to marked as "save_vocab=true"
|
||||
String senderL1;
|
||||
|
||||
/// [senderL1] and [senderL2] are the languages of the sender
|
||||
/// if langCode is not known, the [senderL1] and [senderL2] will be used to help determine the language of the text
|
||||
/// if langCode is known, [senderL1] and [senderL2] will be used to determine whether the tokens need
|
||||
/// pos/mporph tags and whether lemmas are eligible to marked as "save_vocab=true"
|
||||
String senderL2;
|
||||
|
||||
TokensRequestModel({
|
||||
required this.fullText,
|
||||
required this.langCode,
|
||||
required this.senderL1,
|
||||
required this.senderL2,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.fullText: fullText,
|
||||
ModelKey.userL1: senderL1,
|
||||
ModelKey.userL2: senderL2,
|
||||
ModelKey.langCode: langCode,
|
||||
};
|
||||
|
||||
// override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TokensRequestModel &&
|
||||
other.fullText == fullText &&
|
||||
other.senderL1 == senderL1 &&
|
||||
other.senderL2 == senderL2;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => fullText.hashCode ^ senderL1.hashCode ^ senderL2.hashCode;
|
||||
}
|
||||
|
||||
class TokensResponseModel {
|
||||
List<PangeaToken> tokens;
|
||||
String lang;
|
||||
|
||||
TokensResponseModel({required this.tokens, required this.lang});
|
||||
|
||||
factory TokensResponseModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TokensResponseModel(
|
||||
tokens: (json[ModelKey.tokens] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<PangeaToken>(),
|
||||
lang: json[ModelKey.lang],
|
||||
);
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import '../config/environment.dart';
|
||||
import '../models/pangea_token_model.dart';
|
||||
import '../network/requests.dart';
|
||||
import '../network/urls.dart';
|
||||
|
||||
class TokensRepo {
|
||||
static Future<TokensResponseModel> tokenize(
|
||||
String accessToken,
|
||||
TokensRequestModel request,
|
||||
) async {
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.tokenize,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final TokensResponseModel response = TokensResponseModel.fromJson(
|
||||
jsonDecode(
|
||||
utf8.decode(res.bodyBytes).toString(),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.tokens.isEmpty) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception(
|
||||
"empty tokens in tokenize response return",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
class TokensRequestModel {
|
||||
String fullText;
|
||||
String userL1;
|
||||
String userL2;
|
||||
|
||||
TokensRequestModel({
|
||||
required this.fullText,
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.fullText: fullText,
|
||||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
};
|
||||
|
||||
// override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TokensRequestModel &&
|
||||
other.fullText == fullText &&
|
||||
other.userL1 == userL1 &&
|
||||
other.userL2 == userL2;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => fullText.hashCode ^ userL1.hashCode ^ userL2.hashCode;
|
||||
}
|
||||
|
||||
class TokensResponseModel {
|
||||
List<PangeaToken> tokens;
|
||||
String lang;
|
||||
|
||||
TokensResponseModel({required this.tokens, required this.lang});
|
||||
|
||||
factory TokensResponseModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TokensResponseModel(
|
||||
tokens: (json[ModelKey.tokens] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<PangeaToken>(),
|
||||
lang: json[ModelKey.lang],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
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: SizedBox(
|
||||
width: AppConfig.toolbarMinWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.voiceNotAvailable,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchTTSSettings,
|
||||
// commenting out as suspecting this is causing an issue
|
||||
// #freeze-activity
|
||||
style: const ButtonStyle(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(L10n.of(context)!.openVoiceSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
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();
|
||||
|
||||
TtsController() {
|
||||
setupTTS();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await tts.stop();
|
||||
}
|
||||
|
||||
onError(dynamic message) => ErrorHandler.logError(
|
||||
e: message,
|
||||
m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error',
|
||||
data: {
|
||||
'message': message,
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> setupTTS() async {
|
||||
try {
|
||||
tts.setErrorHandler(onError);
|
||||
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
debugger(when: kDebugMode && targetLanguage == null);
|
||||
|
||||
tts.setLanguage(
|
||||
targetLanguage ?? "en",
|
||||
);
|
||||
|
||||
await tts.awaitSpeakCompletion(true);
|
||||
|
||||
final voices = await tts.getVoices;
|
||||
availableLangCodes = (voices as List)
|
||||
.map((v) {
|
||||
// on iOS / web, the codes are in 'locale', but on Android, they are in 'name'
|
||||
final nameCode = v['name']?.split("-").first;
|
||||
final localeCode = v['locale']?.split("-").first;
|
||||
return nameCode.length == 2 ? nameCode : localeCode;
|
||||
})
|
||||
.toSet()
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
debugPrint("availableLangCodes: $availableLangCodes");
|
||||
|
||||
debugger(when: kDebugMode && !isLanguageFullySupported);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
// return type is dynamic but apparent its supposed to be 1
|
||||
// https://pub.dev/packages/flutter_tts
|
||||
final result = await tts.stop();
|
||||
if (result != 1) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Unexpected result from tts.stop',
|
||||
data: {
|
||||
'result': result,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> speak(String text) async {
|
||||
try {
|
||||
stop();
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
final result = await tts.speak(text);
|
||||
|
||||
// return type is dynamic but apparent its supposed to be 1
|
||||
// https://pub.dev/packages/flutter_tts
|
||||
if (result != 1 && !kIsWeb) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Unexpected result from tts.speak',
|
||||
data: {
|
||||
'result': result,
|
||||
'text': text,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
bool get isLanguageFullySupported =>
|
||||
availableLangCodes.contains(targetLanguage);
|
||||
|
||||
Widget get missingVoiceButton => targetLanguage != null &&
|
||||
(kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid)
|
||||
? const SizedBox.shrink()
|
||||
: MissingVoiceButton(
|
||||
targetLangCode: targetLanguage!,
|
||||
);
|
||||
}
|
||||
@ -1,268 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../utils/firebase_analytics.dart';
|
||||
|
||||
//PTODO - auto invite students when you add a space and delete the add_class_and_invite.dart file
|
||||
class AddToSpaceToggles extends StatefulWidget {
|
||||
final String? roomId;
|
||||
final bool startOpen;
|
||||
final String? activeSpaceId;
|
||||
final bool spaceMode;
|
||||
|
||||
const AddToSpaceToggles({
|
||||
super.key,
|
||||
this.roomId,
|
||||
this.startOpen = false,
|
||||
this.activeSpaceId,
|
||||
this.spaceMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
AddToSpaceState createState() => AddToSpaceState();
|
||||
}
|
||||
|
||||
class AddToSpaceState extends State<AddToSpaceToggles> {
|
||||
late Room? room;
|
||||
late Room? parent;
|
||||
late List<Room> possibleParents;
|
||||
late bool isOpen;
|
||||
late bool isSuggested;
|
||||
|
||||
AddToSpaceState({Key? key});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initialize();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AddToSpaceToggles oldWidget) {
|
||||
if (oldWidget.roomId != widget.roomId) {
|
||||
initialize();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
//if roomId is null, it means this widget is being used in the creation flow
|
||||
room = widget.roomId != null
|
||||
? Matrix.of(context).client.getRoomById(widget.roomId!)
|
||||
: null;
|
||||
|
||||
isSuggested = true;
|
||||
room?.isSuggested().then((value) => isSuggested = value);
|
||||
|
||||
possibleParents = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
(Room r) => r.isSpace && widget.roomId != r.id,
|
||||
)
|
||||
.toList();
|
||||
|
||||
parent = widget.roomId != null
|
||||
? possibleParents.firstWhereOrNull(
|
||||
(r) => r.spaceChildren.any((room) => room.roomId == widget.roomId),
|
||||
)
|
||||
: null;
|
||||
|
||||
//sort possibleParents
|
||||
//if possibleParent in parents, put first
|
||||
//use sort but use any instead of contains because contains uses == and we want to compare by id
|
||||
possibleParents.sort((a, b) {
|
||||
if (parent?.id == a.id) {
|
||||
return -1;
|
||||
} else if (parent?.id == b.id) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.compareTo(b.name);
|
||||
}
|
||||
});
|
||||
|
||||
isOpen = widget.startOpen;
|
||||
|
||||
if (widget.activeSpaceId != null) {
|
||||
final activeSpace =
|
||||
Matrix.of(context).client.getRoomById(widget.activeSpaceId!);
|
||||
if (activeSpace == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('activeSpaceId ${widget.activeSpaceId} not found'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (activeSpace.canSendEvent(EventTypes.SpaceChild)) {
|
||||
parent = activeSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addSingleSpace(String roomToAddId, Room newParent) async {
|
||||
GoogleAnalytics.addParent(roomToAddId, newParent.classCode);
|
||||
await newParent.pangeaSetSpaceChild(
|
||||
roomToAddId,
|
||||
suggested: isSuggested,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addSpaces(String roomToAddId) async {
|
||||
if (parent == null) return;
|
||||
await _addSingleSpace(roomToAddId, parent!);
|
||||
}
|
||||
|
||||
Future<void> handleAdd(bool add, Room possibleParent) async {
|
||||
//in this case, the room has already been made so we handle adding as it happens
|
||||
if (room != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => add
|
||||
? _addSingleSpace(room!.id, possibleParent)
|
||||
: possibleParent.removeSpaceChild(room!.id),
|
||||
onError: (e) {
|
||||
// if error occurs, do not change value of toggle
|
||||
add = !add;
|
||||
return (e as Object?)?.toLocalizedString(context) ??
|
||||
e?.toString() ??
|
||||
L10n.of(context)!.oopsSomethingWentWrong;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(
|
||||
() => add ? parent = possibleParent : parent = null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget getAddToSpaceToggleItem(int index) {
|
||||
final Room possibleParent = possibleParents[index];
|
||||
final bool canAdd = possibleParent.canAddAsParentOf(
|
||||
room,
|
||||
spaceMode: widget.spaceMode,
|
||||
);
|
||||
|
||||
return Opacity(
|
||||
opacity: canAdd ? 1 : 0.5,
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: possibleParent.nameAndRoomTypeIcon(),
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
value: parent?.id == possibleParent.id,
|
||||
onChanged: (bool add) => canAdd
|
||||
? handleAdd(add, possibleParent)
|
||||
: ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noPermission),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 0.5,
|
||||
color: Theme.of(context).colorScheme.secondary.withAlpha(25),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setSuggested(bool suggested) async {
|
||||
setState(() => isSuggested = suggested);
|
||||
if (room != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async => await room?.setSuggested(suggested),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.addToSpace,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.addSpaceToSpaceDesc
|
||||
: L10n.of(context)!.addChatToSpaceDesc,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: const Icon(Icons.workspaces_outlined),
|
||||
),
|
||||
trailing: Icon(
|
||||
isOpen
|
||||
? Icons.keyboard_arrow_down_outlined
|
||||
: Icons.keyboard_arrow_right_outlined,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => isOpen = !isOpen);
|
||||
},
|
||||
),
|
||||
if (isOpen) ...[
|
||||
const Divider(height: 1),
|
||||
possibleParents.isNotEmpty
|
||||
? Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.suggestToSpace
|
||||
: L10n.of(context)!.suggestToChat,
|
||||
),
|
||||
secondary: Icon(
|
||||
isSuggested
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.spaceMode || (room?.isSpace ?? false)
|
||||
? L10n.of(context)!.suggestToSpaceDesc
|
||||
: L10n.of(context)!.suggestToChatDesc,
|
||||
),
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
value: isSuggested,
|
||||
onChanged: (bool add) => setSuggested(add),
|
||||
),
|
||||
Divider(
|
||||
height: 0.5,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary.withAlpha(25),
|
||||
),
|
||||
...possibleParents.mapIndexed(
|
||||
(index, _) => getAddToSpaceToggleItem(index),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
L10n.of(context)!.inNoSpaces,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue