in the midst of it
This commit is contained in:
parent
695374ee46
commit
361690935d
16 changed files with 759 additions and 77 deletions
|
|
@ -3956,5 +3956,6 @@
|
|||
"inNoSpaces": "You are not a member of any classes or exchanges",
|
||||
"successfullySubscribed": "You have successfully subscribed!",
|
||||
"clickToManageSubscription": "Click here to manage your subscription.",
|
||||
"emptyInviteWarning": "Add this chat to a class or exchange to invite other users."
|
||||
"emptyInviteWarning": "Add this chat to a class or exchange to invite other users.",
|
||||
"errorGettingAudio": "Error getting audio. Please refresh and try again."
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import 'package:animations/animations.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_actions.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../config/themes.dart';
|
||||
import 'chat.dart';
|
||||
import 'input_bar.dart';
|
||||
|
|
@ -58,18 +58,21 @@ class ChatInputRow extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: TextButton(
|
||||
onPressed: controller.forwardEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(Icons.keyboard_arrow_left_outlined),
|
||||
Text(L10n.of(context)!.forward),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
PangeaMessageActions(chatController: controller),
|
||||
// SizedBox(
|
||||
// height: 56,
|
||||
// child: TextButton(
|
||||
// onPressed: controller.forwardEventsAction,
|
||||
// child: Row(
|
||||
// children: <Widget>[
|
||||
// const Icon(Icons.keyboard_arrow_left_outlined),
|
||||
// Text(L10n.of(context)!.forward),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
controller.selectedEvents.length == 1
|
||||
? controller.selectedEvents.first
|
||||
.getDisplayEvent(controller.timeline!)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ import '../../utils/stream_extension.dart';
|
|||
import 'chat_emoji_picker.dart';
|
||||
import 'chat_input_row.dart';
|
||||
|
||||
enum _EventContextAction { info, report }
|
||||
//#Pangea
|
||||
// enum _EventContextAction { info, report }
|
||||
enum _EventContextAction { info, forward, report }
|
||||
//Pangea#
|
||||
|
||||
class ChatView extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -85,18 +88,34 @@ class ChatView extends StatelessWidget {
|
|||
case _EventContextAction.report:
|
||||
controller.reportEventAction();
|
||||
break;
|
||||
// #Pangea
|
||||
case _EventContextAction.forward:
|
||||
controller.forwardEventsAction();
|
||||
break;
|
||||
// Pangea#
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
// #Pangea
|
||||
// PopupMenuItem(
|
||||
// value: _EventContextAction.info,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.info_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context)!.messageInfo),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: _EventContextAction.info,
|
||||
value: _EventContextAction.forward,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const Icon(Icons.forward),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.messageInfo),
|
||||
Text(L10n.of(context)!.forward),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:fluffychat/pangea/widgets/flag.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
|
|
@ -55,3 +55,70 @@ class LanguageDisplayToggle extends StatelessWidget {
|
|||
// );
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageToggleSwitch extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const LanguageToggleSwitch({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
_LanguageToggleSwitchState createState() => _LanguageToggleSwitchState();
|
||||
}
|
||||
|
||||
class _LanguageToggleSwitchState extends State<LanguageToggleSwitch> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius =
|
||||
BorderRadius.circular(20.0); // Use the same radius as your LanguageFlag
|
||||
|
||||
return Tooltip(
|
||||
message: L10n.of(context)!.toggleLanguages,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.transparent, // No background color
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
padding: EdgeInsets.zero, // Aligns with your custom padding
|
||||
),
|
||||
onPressed: _toggleLanguage, // Use the onTap logic for onPressed
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.background, // Adapt to your app theme or custom color
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
//trranslatte icon
|
||||
Opacity(
|
||||
opacity: isL1Selected ? 1.0 : 0.6,
|
||||
child: LanguageFlag(
|
||||
language: widget.controller.choreographer.l1Lang,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0), // Spacing between flags
|
||||
Opacity(
|
||||
opacity: isL1Selected ? 0.6 : 1.0,
|
||||
child: LanguageFlag(
|
||||
language: widget.controller.choreographer.l2Lang,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isL1Selected =>
|
||||
widget.controller.choreographer.messageOptions.isTranslationOn;
|
||||
|
||||
void _toggleLanguage() {
|
||||
setState(() {
|
||||
widget.controller.choreographer.messageOptions
|
||||
.toggleSelectedDisplayLang();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ModelKey {
|
|||
static const String word = "word";
|
||||
static const String lang = "lang";
|
||||
static const String deepL = "deepl";
|
||||
static const String langCode = 'langCode';
|
||||
static const String langCode = 'lang_code';
|
||||
static const String wordLang = "word_lang";
|
||||
static const String lemma = "lemma";
|
||||
static const String saveVocab = "save_vocab";
|
||||
|
|
@ -87,4 +87,6 @@ class ModelKey {
|
|||
static const String currentText = "current";
|
||||
static const String bestContinuance = "best_continuance";
|
||||
static const String feedbackLang = "feedback_lang";
|
||||
|
||||
static const String transcription = "transcription";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ class PangeaEventTypes {
|
|||
|
||||
static const vocab = "p.vocab";
|
||||
static const roomInfo = "pangea.roomtopic";
|
||||
|
||||
static const audio = "p.audio";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
|
|||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/user_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/word_net_controller.dart';
|
||||
import 'package:fluffychat/pangea/guard/p_vguard.dart';
|
||||
|
|
@ -44,6 +45,7 @@ class PangeaController {
|
|||
late ITFeedbackController itFeedback;
|
||||
late InstructionsController instructions;
|
||||
late SubscriptionController subscriptionController;
|
||||
late TextToSpeechController textToSpeech;
|
||||
|
||||
///store Services
|
||||
late PLocalStore pStoreService;
|
||||
|
|
@ -89,6 +91,7 @@ class PangeaController {
|
|||
instructions = InstructionsController(this);
|
||||
subscriptionController = SubscriptionController(this);
|
||||
itFeedback = ITFeedbackController(this);
|
||||
textToSpeech = TextToSpeechController(this);
|
||||
PAuthGaurd.pController = this;
|
||||
}
|
||||
|
||||
|
|
|
|||
125
lib/pangea/controllers/text_to_speech_controller.dart
Normal file
125
lib/pangea/controllers/text_to_speech_controller.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
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/network/urls.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../network/requests.dart';
|
||||
|
||||
class TextToSpeechRequest {
|
||||
String text;
|
||||
String langCode;
|
||||
|
||||
TextToSpeechRequest({required this.text, required this.langCode});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TextToSpeechRequest &&
|
||||
other.text == text &&
|
||||
other.langCode == langCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => text.hashCode ^ langCode.hashCode;
|
||||
}
|
||||
|
||||
class TextToSpeechResponse {
|
||||
String audioContent;
|
||||
String mediaType;
|
||||
int durationMillis;
|
||||
List<int> waveform;
|
||||
|
||||
TextToSpeechResponse({
|
||||
required this.audioContent,
|
||||
required this.mediaType,
|
||||
required this.durationMillis,
|
||||
required this.waveform,
|
||||
});
|
||||
|
||||
factory TextToSpeechResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
TextToSpeechResponse(
|
||||
audioContent: json["audio_content"],
|
||||
mediaType: json["media_type"],
|
||||
durationMillis: json["duration_millis"],
|
||||
waveform: List<int>.from(json["wave_form"]),
|
||||
);
|
||||
}
|
||||
|
||||
class _TextToSpeechCacheItem {
|
||||
Future<TextToSpeechResponse> data;
|
||||
|
||||
_TextToSpeechCacheItem({
|
||||
required this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class TextToSpeechController {
|
||||
static final Map<TextToSpeechRequest, _TextToSpeechCacheItem> _cache = {};
|
||||
late final PangeaController _pangeaController;
|
||||
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
TextToSpeechController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 15); // Adjust the duration as needed
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
}
|
||||
|
||||
Future<TextToSpeechResponse> get(
|
||||
TextToSpeechRequest params,
|
||||
) async {
|
||||
if (_cache.containsKey(params)) {
|
||||
return _cache[params]!.data;
|
||||
} else {
|
||||
final Future<TextToSpeechResponse> response = _fetchResponse(
|
||||
await _pangeaController.userController.accessToken,
|
||||
params,
|
||||
);
|
||||
_cache[params] = _TextToSpeechCacheItem(data: response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<TextToSpeechResponse> _fetchResponse(
|
||||
String accessToken,
|
||||
TextToSpeechRequest params,
|
||||
) async {
|
||||
final Requests request = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
|
||||
final Response res = await request.post(
|
||||
url: PApiUrls.textToSpeech,
|
||||
body: params.toJson(),
|
||||
);
|
||||
|
||||
final Map<String, dynamic> json = jsonDecode(res.body);
|
||||
|
||||
return TextToSpeechResponse.fromJson(json);
|
||||
}
|
||||
}
|
||||
9
lib/pangea/models/pangea_audio_events.dart
Normal file
9
lib/pangea/models/pangea_audio_events.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// relates to a pangea representation event
|
||||
// the matrix even fits the form of a regular matrix audio event
|
||||
// but with something to distinguish it as a pangea audio event
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PangeaAudioEvent {
|
||||
Event? _event;
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_message_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/message_data_models.dart';
|
||||
|
|
@ -81,6 +83,116 @@ class PangeaMessageEvent {
|
|||
return true;
|
||||
}
|
||||
|
||||
//get audio for text and language
|
||||
//if no audio exists, create it
|
||||
//if audio exists, return it
|
||||
Future<String?> getAudioGlobal(String langCode) async {
|
||||
// try {
|
||||
final String text = representationByLanguage(langCode)?.text ?? body;
|
||||
|
||||
final local = getAudioLocal(langCode, text);
|
||||
|
||||
if (local != null) return Future.value(local.eventId);
|
||||
|
||||
final TextToSpeechRequest params = TextToSpeechRequest(
|
||||
text: text,
|
||||
langCode: langCode,
|
||||
);
|
||||
|
||||
final TextToSpeechResponse response =
|
||||
await MatrixState.pangeaController.textToSpeech.get(
|
||||
params,
|
||||
);
|
||||
|
||||
if (response.mediaType != 'audio/ogg') {
|
||||
throw Exception('Unexpected media type: ${response.mediaType}');
|
||||
}
|
||||
|
||||
final audioBytes = base64.decode(response.audioContent);
|
||||
|
||||
// from text, trim whitespace, remove special characters, and limit to 20 characters
|
||||
// final fileName =
|
||||
// text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20);
|
||||
final fileName = "audio_for_${eventId}_$langCode";
|
||||
|
||||
final file = MatrixAudioFile(
|
||||
bytes: audioBytes,
|
||||
name: fileName,
|
||||
);
|
||||
|
||||
return room.sendFileEvent(
|
||||
file,
|
||||
inReplyTo: _event,
|
||||
extraContent: {
|
||||
'info': {
|
||||
...file.info,
|
||||
'duration': response.durationMillis,
|
||||
},
|
||||
'org.matrix.msc3245.voice': {},
|
||||
'org.matrix.msc1767.audio': {
|
||||
'duration': response.durationMillis,
|
||||
'waveform': null,
|
||||
// 'waveform': response.waveform,
|
||||
},
|
||||
'transcription': {
|
||||
ModelKey.text: text,
|
||||
ModelKey.langCode: langCode,
|
||||
},
|
||||
},
|
||||
).timeout(
|
||||
Durations.long4,
|
||||
onTimeout: () {
|
||||
debugPrint("timeout in getAudioGlobal");
|
||||
return null;
|
||||
},
|
||||
).then((eventId) {
|
||||
debugPrint("eventId in getAudioGlobal $eventId");
|
||||
return eventId;
|
||||
}).catchError((err, s) {
|
||||
debugPrint("error in getAudioGlobal");
|
||||
debugPrint(err);
|
||||
debugPrint(s);
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
});
|
||||
|
||||
// } catch (err, s) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(
|
||||
// e: err,
|
||||
// s: s,
|
||||
// );
|
||||
// return Future.value(null);
|
||||
// }
|
||||
}
|
||||
|
||||
Event? getAudioLocal(String langCode, String text) {
|
||||
return allAudio.firstWhereOrNull(
|
||||
(element) {
|
||||
// Safely access the transcription map
|
||||
final transcription =
|
||||
element.content.tryGet<Map<String, String>>(ModelKey.transcription);
|
||||
if (transcription == null) {
|
||||
// If transcription is null, this element does not match.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safely get language code and text from the transcription
|
||||
final elementLangCode = transcription.tryGet(ModelKey.langCode);
|
||||
final elementText = transcription.tryGet(ModelKey.text);
|
||||
|
||||
// Check if both language code and text match
|
||||
return elementLangCode == langCode && elementText == text;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// get audio events that are related to this event
|
||||
Set<Event> get allAudio => _latestEdit.aggregatedEvents(
|
||||
timeline,
|
||||
EventTypes.Message,
|
||||
);
|
||||
|
||||
List<RepresentationEvent>? _representations;
|
||||
List<RepresentationEvent> get representations {
|
||||
if (_representations != null) return _representations!;
|
||||
|
|
@ -188,11 +300,6 @@ class PangeaMessageEvent {
|
|||
|
||||
RepresentationEvent? rep = representationByLanguage(langCode);
|
||||
|
||||
//if event is less than 1 minute old, then print new event
|
||||
if (isNew) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
while ((isNew || eventId.contains("web")) && tries < 20) {
|
||||
if (rep != null) return rep;
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ class PApiUrls {
|
|||
static String firstStep = "/it_initialstep";
|
||||
static String subseqStep = "/it_step";
|
||||
|
||||
static String textToSpeech = "$choreoBaseApi/text_to_speech";
|
||||
|
||||
///-------------------------------- revenue cat --------------------------
|
||||
static String rcApiV1 = "https://api.revenuecat.com/v1";
|
||||
static String rcApiV2 =
|
||||
|
|
|
|||
72
lib/pangea/repo/image_repo.dart
Normal file
72
lib/pangea/repo/image_repo.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../config/environment.dart';
|
||||
import '../network/requests.dart';
|
||||
|
||||
class GenerateImageeResponse {
|
||||
final String imageUrl;
|
||||
final String prompt;
|
||||
|
||||
GenerateImageeResponse({
|
||||
required this.imageUrl,
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
factory GenerateImageeResponse.fromJson(Map<String, dynamic> json) {
|
||||
return GenerateImageeResponse(
|
||||
imageUrl: json['image_url'],
|
||||
prompt: json['prompt'],
|
||||
);
|
||||
}
|
||||
|
||||
factory GenerateImageeResponse.error() {
|
||||
return GenerateImageeResponse(
|
||||
imageUrl: 'https://i.imgur.com/2L2JYqk.png',
|
||||
prompt: 'Error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateImageRequest {
|
||||
String prompt;
|
||||
|
||||
GenerateImageRequest({required this.prompt});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'prompt': prompt,
|
||||
};
|
||||
}
|
||||
|
||||
class ImageRepo {
|
||||
static Future<GenerateImageeResponse> fetchImage(
|
||||
GenerateImageRequest request) async {
|
||||
final Requests req =
|
||||
Requests(baseUrl: Environment.choreoApi); // Set your API base URL
|
||||
final requestBody = request.toJson();
|
||||
|
||||
try {
|
||||
final Response res = await req.post(
|
||||
url: '/generate-image/', // Endpoint in your FastAPI server
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
return GenerateImageeResponse.fromJson(
|
||||
decodedBody); // Convert response to ImageModel
|
||||
} else {
|
||||
throw Exception('Failed to load image');
|
||||
}
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack, data: requestBody);
|
||||
return GenerateImageeResponse
|
||||
.error(); // Return an error model or handle accordingly
|
||||
}
|
||||
}
|
||||
}
|
||||
66
lib/pangea/repo/text_to_speech_repo.dart
Normal file
66
lib/pangea/repo/text_to_speech_repo.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// import 'dart:async';
|
||||
// import 'dart:convert';
|
||||
|
||||
// import 'package:fluffychat/pangea/config/environment.dart';
|
||||
// import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
// import 'package:fluffychat/pangea/network/urls.dart';
|
||||
// import 'package:http/http.dart';
|
||||
|
||||
// import '../network/requests.dart';
|
||||
|
||||
// class TextToSpeechRequest {
|
||||
// String text;
|
||||
// String langCode;
|
||||
|
||||
// TextToSpeechRequest({required this.text, required this.langCode});
|
||||
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// ModelKey.text: text,
|
||||
// ModelKey.langCode: langCode,
|
||||
// };
|
||||
// }
|
||||
|
||||
// class TextToSpeechResponse {
|
||||
// String audioContent;
|
||||
// String mediaType;
|
||||
// int durationMillis;
|
||||
// List<int> waveform;
|
||||
|
||||
// TextToSpeechResponse({
|
||||
// required this.audioContent,
|
||||
// required this.mediaType,
|
||||
// required this.durationMillis,
|
||||
// required this.waveform,
|
||||
// });
|
||||
|
||||
// factory TextToSpeechResponse.fromJson(
|
||||
// Map<String, dynamic> json,
|
||||
// ) =>
|
||||
// TextToSpeechResponse(
|
||||
// audioContent: json["audio_content"],
|
||||
// mediaType: json["media_type"],
|
||||
// durationMillis: json["duration_millis"],
|
||||
// waveform: List<int>.from(json["wave_form"]),
|
||||
// );
|
||||
// }
|
||||
|
||||
// class TextToSpeechService {
|
||||
// static Future<TextToSpeechResponse> get({
|
||||
// required String accessToken,
|
||||
// required TextToSpeechRequest params,
|
||||
// }) async {
|
||||
// final Requests request = Requests(
|
||||
// choreoApiKey: Environment.choreoApiKey,
|
||||
// accessToken: accessToken,
|
||||
// );
|
||||
|
||||
// final Response res = await request.post(
|
||||
// url: PApiUrls.textToSpeech,
|
||||
// body: params.toJson(),
|
||||
// );
|
||||
|
||||
// final Map<String, dynamic> json = jsonDecode(res.body);
|
||||
|
||||
// return TextToSpeechResponse.fromJson(json);
|
||||
// }
|
||||
// }
|
||||
27
lib/pangea/widgets/chat/message_actions.dart
Normal file
27
lib/pangea/widgets/chat/message_actions.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PangeaMessageActions extends StatelessWidget {
|
||||
final ChatController chatController;
|
||||
|
||||
const PangeaMessageActions({super.key, required this.chatController});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
LanguageToggleSwitch(controller: chatController),
|
||||
TextToSpeechButton(
|
||||
controller: chatController,
|
||||
),
|
||||
// IconButton(
|
||||
// icon: Icon(Icons.mic),
|
||||
// onPressed: chatController.onMicTap,
|
||||
// ),
|
||||
// Add more IconButton widgets here
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/pangea/widgets/chat/text_to_speech_button.dart
Normal file
128
lib/pangea/widgets/chat/text_to_speech_button.dart
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class TextToSpeechButton extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const TextToSpeechButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
_TextToSpeechButtonState createState() => _TextToSpeechButtonState();
|
||||
}
|
||||
|
||||
class _TextToSpeechButtonState extends State<TextToSpeechButton> {
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _playSpeech() {
|
||||
try {
|
||||
final String langCode = widget.controller.choreographer.messageOptions
|
||||
.selectedDisplayLang?.langCode ??
|
||||
widget.controller.choreographer.l2LangCode ??
|
||||
'en';
|
||||
final Event event = widget.controller.selectedEvents.first;
|
||||
|
||||
PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: widget.controller.timeline!,
|
||||
ownMessage: event.senderId == Matrix.of(context).client.userID,
|
||||
selected: true,
|
||||
).getAudioGlobal(langCode);
|
||||
|
||||
// final String? text = PangeaMessageEvent(
|
||||
// event: event,
|
||||
// timeline: widget.controller.timeline!,
|
||||
// ownMessage: event.senderId == Matrix.of(context).client.userID,
|
||||
// selected: true,
|
||||
// ).representationByLanguage(langCode)?.text;
|
||||
|
||||
// if (text == null || text.isEmpty) {
|
||||
// throw Exception("text is null or empty in text_to_speech_button.dart");
|
||||
// }
|
||||
|
||||
// final TextToSpeechRequest params = TextToSpeechRequest(
|
||||
// text: text,
|
||||
// langCode: widget.controller.choreographer.messageOptions
|
||||
// .selectedDisplayLang?.langCode ??
|
||||
// widget.controller.choreographer.l2LangCode ??
|
||||
// LanguageKeys.unknownLanguage,
|
||||
// );
|
||||
|
||||
// final TextToSpeechResponse response = await TextToSpeechService.get(
|
||||
// accessToken:
|
||||
// await MatrixState.pangeaController.userController.accessToken,
|
||||
// params: params,
|
||||
// );
|
||||
|
||||
// if (response.mediaType != 'audio/ogg') {
|
||||
// throw Exception('Unexpected media type: ${response.mediaType}');
|
||||
// }
|
||||
|
||||
// // Decode the base64 audio content to bytes
|
||||
// final audioBytes = base64.decode(response.audioContent);
|
||||
|
||||
// final encoding = Uri.dataFromBytes(audioBytes);
|
||||
// final uri = AudioSource.uri(encoding);
|
||||
// // gets here without problems
|
||||
|
||||
// await _audioPlayer.setAudioSource(uri);
|
||||
// await _audioPlayer.play();
|
||||
|
||||
// final audioBytes = base64.decode(response.audioContent);
|
||||
// final tempDir = await getTemporaryDirectory();
|
||||
// final file = File('${tempDir.path}/speech.ogg');
|
||||
// await file.writeAsBytes(audioBytes);
|
||||
|
||||
// await _audioPlayer.setFilePath(file.path);
|
||||
|
||||
// await _audioPlayer.play();
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context)!.errorGettingAudio,
|
||||
),
|
||||
),
|
||||
);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(),
|
||||
s: StackTrace.current,
|
||||
m: 'text is null or empty in text_to_speech_button.dart',
|
||||
data: {
|
||||
'event': widget.controller.selectedEvents.first,
|
||||
'langCode': widget.controller.choreographer.messageOptions
|
||||
.selectedDisplayLang?.langCode ??
|
||||
widget.controller.choreographer.l2LangCode ??
|
||||
LanguageKeys.unknownLanguage,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: _playSpeech,
|
||||
child: const Text('Convert to Speech'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -765,7 +765,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
|
|
@ -1539,7 +1540,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"bo": [
|
||||
|
|
@ -2313,7 +2315,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
|
|
@ -3082,7 +3085,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
|
|
@ -3851,7 +3855,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"de": [
|
||||
|
|
@ -4620,7 +4625,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
|
@ -5394,7 +5400,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"eo": [
|
||||
|
|
@ -6163,7 +6170,12 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"et": [
|
||||
|
|
@ -6932,7 +6944,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
|
|
@ -7701,7 +7714,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
|
|
@ -8470,7 +8484,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
|
|
@ -9239,7 +9254,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
|
@ -10008,7 +10024,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ga": [
|
||||
|
|
@ -10777,7 +10794,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
|
|
@ -11546,7 +11564,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"he": [
|
||||
|
|
@ -12315,7 +12334,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
|
|
@ -13089,7 +13109,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"hr": [
|
||||
|
|
@ -13858,7 +13879,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"hu": [
|
||||
|
|
@ -14627,7 +14649,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"id": [
|
||||
|
|
@ -15396,7 +15419,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ie": [
|
||||
|
|
@ -16167,7 +16191,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
|
@ -16936,7 +16961,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
|
|
@ -17705,7 +17731,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
|
|
@ -18474,7 +18501,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
|
|
@ -19243,7 +19271,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"lv": [
|
||||
|
|
@ -20017,7 +20046,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"nb": [
|
||||
|
|
@ -20786,7 +20816,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
|
@ -21555,7 +21586,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
|
|
@ -22324,7 +22356,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
|
@ -23098,7 +23131,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"pt_BR": [
|
||||
|
|
@ -23867,7 +23901,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"pt_PT": [
|
||||
|
|
@ -24636,7 +24671,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ro": [
|
||||
|
|
@ -25405,7 +25441,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
|
@ -26174,7 +26211,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"sk": [
|
||||
|
|
@ -26944,7 +26982,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
|
|
@ -27716,7 +27755,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"sr": [
|
||||
|
|
@ -28485,7 +28525,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"sv": [
|
||||
|
|
@ -29254,7 +29295,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"ta": [
|
||||
|
|
@ -30028,7 +30070,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"th": [
|
||||
|
|
@ -30802,7 +30845,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
|
|
@ -31571,7 +31615,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
|
|
@ -32340,7 +32385,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
|
|
@ -33112,7 +33158,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
|
@ -33881,7 +33928,8 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
|
@ -34650,6 +34698,7 @@
|
|||
"activateTrial",
|
||||
"successfullySubscribed",
|
||||
"clickToManageSubscription",
|
||||
"emptyInviteWarning"
|
||||
"emptyInviteWarning",
|
||||
"errorGettingAudio"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue