From 68b1382ba205fa2795a9f444d704201ed5a17999 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:57:45 -0400 Subject: [PATCH 1/4] feat(phonetic_transcription): repo set up --- lib/pangea/common/network/urls.dart | 3 + .../phonetic_transcription_repo.dart | 66 +++++++++++++++++++ .../phonetic_transcription_request.dart | 33 ++++++++++ .../phonetic_transcription_response.dart | 55 ++++++++++++++++ .../test_phonetic_transcription_api.dart | 0 5 files changed, 157 insertions(+) create mode 100644 lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart create mode 100644 lib/pangea/phonetic_transcription/phonetic_transcription_request.dart create mode 100644 lib/pangea/phonetic_transcription/phonetic_transcription_response.dart create mode 100644 lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index a667066c8..5bb4f2c86 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -86,4 +86,7 @@ class PApiUrls { static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial"; static String rcSubscription = PApiUrls.subscriptionEndpoint; + + static String phoneticTranscription = + "${PApiUrls.choreoEndpoint}/phonetic_transcription"; } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart new file mode 100644 index 000000000..30eac8302 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + +class PhoneticTranscriptionRepo { + static final GetStorage _storage = + GetStorage('phonetic_transcription_storage'); + + static void set(PhoneticTranscriptionRequest request, + PhoneticTranscriptionResponse response) { + response.expireAt ??= DateTime.now().add(const Duration(days: 100)); + _storage.write(request.storageKey, response.toJson()); + } + + static Future _fetch( + PhoneticTranscriptionRequest request) async { + final cachedJson = _storage.read(request.storageKey); + final cached = cachedJson == null + ? null + : PhoneticTranscriptionResponse.fromJson(cachedJson); + + if (cached != null) { + if (DateTime.now().isBefore(cached.expireAt!)) { + return cached; + } else { + _storage.remove(request.storageKey); + } + } + + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.phoneticTranscription, + body: request.toJson(), + ); + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + final response = PhoneticTranscriptionResponse.fromJson(decodedBody); + set(request, response); + return response; + } + + static Future get( + PhoneticTranscriptionRequest request) async { + try { + return await _fetch(request); + } catch (e) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, data: request.toJson()); + rethrow; + } + } +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart new file mode 100644 index 000000000..02dfcb7c7 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart @@ -0,0 +1,33 @@ +class PhoneticTranscriptionRequest { + final String l1; + final String l2; + final String content; + final bool requiresTokenization; + + PhoneticTranscriptionRequest({ + required this.l1, + required this.l2, + required this.content, + this.requiresTokenization = true, + }); + + factory PhoneticTranscriptionRequest.fromJson(Map json) { + return PhoneticTranscriptionRequest( + l1: json['l1'] as String, + l2: json['l2'] as String, + content: json['content'] as String, + requiresTokenization: json['requires_tokenization'] ?? true, + ); + } + + Map toJson() { + return { + 'l1': l1, + 'l2': l2, + 'content': content, + 'requires_tokenization': requiresTokenization, + }; + } + + String get storageKey => 'l1:$l1,l2:$l2,content:$content'; +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart new file mode 100644 index 000000000..9efce60d4 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart @@ -0,0 +1,55 @@ +class PhoneticTranscriptionResponse { + final Map arc; + final Map content; + final Map tokenization; + final Map phoneticTranscriptionResult; + DateTime? expireAt; + + PhoneticTranscriptionResponse({ + required this.arc, + required this.content, + required this.tokenization, + required this.phoneticTranscriptionResult, + this.expireAt, + }); + + factory PhoneticTranscriptionResponse.fromJson(Map json) { + return PhoneticTranscriptionResponse( + arc: Map.from(json['arc'] as Map), + content: Map.from(json['content'] as Map), + tokenization: Map.from(json['tokenization'] as Map), + phoneticTranscriptionResult: Map.from( + json['phonetic_transcription_result'] as Map), + expireAt: json['expireAt'] == null + ? null + : DateTime.parse(json['expireAt'] as String), + ); + } + + Map toJson() { + return { + 'arc': arc, + 'content': content, + 'tokenization': tokenization, + 'phonetic_transcription_result': phoneticTranscriptionResult, + 'expireAt': expireAt?.toIso8601String(), + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PhoneticTranscriptionResponse && + runtimeType == other.runtimeType && + arc == other.arc && + content == other.content && + tokenization == other.tokenization && + phoneticTranscriptionResult == other.phoneticTranscriptionResult; + + @override + int get hashCode => + arc.hashCode ^ + content.hashCode ^ + tokenization.hashCode ^ + phoneticTranscriptionResult.hashCode; +} diff --git a/lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart b/lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart new file mode 100644 index 000000000..e69de29bb From 40a6e5a10bb67fc05561cc8b37c3e380a8bd64e1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 16 Jun 2025 13:52:32 -0400 Subject: [PATCH 2/4] chore: started adding widget to show phonetic transcription --- lib/l10n/intl_en.arb | 3 +- .../vocab_analytics_details_view.dart | 10 ++ .../phonetic_transcription_repo.dart | 20 ++-- .../phonetic_transcription_widget.dart | 106 ++++++++++++++++++ .../test_phonetic_transcription_api.dart | 0 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart delete mode 100644 lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 50166f59f..a7e110b90 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5016,5 +5016,6 @@ "directMessage": "Direct Message", "newDirectMessage": "New direct message", "speakingExercisesTooltip": "Speaking practice", - "noChatsFoundHereYet": "No chats found here yet" + "noChatsFoundHereYet": "No chats found here yet", + "phoneticTranscriptionError": "Phonetic transcription failed" } \ No newline at end of file diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 56fabe42c..4f65589cc 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_icon.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -26,6 +27,9 @@ class VocabDetailsView extends StatelessWidget { ConstructUses get _construct => constructId.constructUses; + String? get _userL1 => + MatrixState.pangeaController.languageController.userL1?.langCode; + /// Get the language code for the current lemma String? get _userL2 => MatrixState.pangeaController.languageController.userL2?.langCode; @@ -60,6 +64,12 @@ class VocabDetailsView extends StatelessWidget { ), subtitle: Column( children: [ + if (_userL1 != null && _userL2 != null) + PhoneticTranscription( + text: _construct.lemma, + l1: _userL1!, + l2: _userL2!, + ), Row( mainAxisSize: MainAxisSize.min, spacing: 8.0, diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart index 30eac8302..124389eed 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -1,6 +1,11 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:flutter/foundation.dart'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; @@ -8,22 +13,22 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:http/http.dart'; class PhoneticTranscriptionRepo { static final GetStorage _storage = GetStorage('phonetic_transcription_storage'); - static void set(PhoneticTranscriptionRequest request, - PhoneticTranscriptionResponse response) { + static void set( + PhoneticTranscriptionRequest request, + PhoneticTranscriptionResponse response, + ) { response.expireAt ??= DateTime.now().add(const Duration(days: 100)); _storage.write(request.storageKey, response.toJson()); } static Future _fetch( - PhoneticTranscriptionRequest request) async { + PhoneticTranscriptionRequest request, + ) async { final cachedJson = _storage.read(request.storageKey); final cached = cachedJson == null ? null @@ -54,7 +59,8 @@ class PhoneticTranscriptionRepo { } static Future get( - PhoneticTranscriptionRequest request) async { + PhoneticTranscriptionRequest request, + ) async { try { return await _fetch(request); } catch (e) { diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart new file mode 100644 index 000000000..b8d7f0d77 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; + +class PhoneticTranscription extends StatefulWidget { + final String text; + final String l1; + final String l2; + + const PhoneticTranscription({ + super.key, + required this.text, + required this.l1, + required this.l2, + }); + + @override + State createState() => PhoneticTranscriptionState(); +} + +class PhoneticTranscriptionState extends State { + bool _loading = false; + String? error; + + PhoneticTranscriptionResponse? _response; + + @override + void initState() { + super.initState(); + _fetchPhoneticTranscription(); + } + + @override + void didUpdateWidget(covariant PhoneticTranscription oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text || + oldWidget.l1 != widget.l1 || + oldWidget.l2 != widget.l2) { + _fetchPhoneticTranscription(); + } + } + + Future _fetchPhoneticTranscription() async { + final PhoneticTranscriptionRequest request = PhoneticTranscriptionRequest( + l1: widget.l1, + l2: widget.l2, + content: widget.text, + ); + + try { + setState(() { + _loading = true; + error = null; + }); + + _response = await PhoneticTranscriptionRepo.get(request); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: request.toJson(), + ); + error = e.toString(); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (error != null) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + size: 16.0, + ), + const SizedBox(width: 8), + Text( + L10n.of(context).phoneticTranscriptionError, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ); + } + + if (_loading || _response == null) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + return Text( + 'Phonetic transcription for "${widget.text}" in ${widget.l2}', + style: Theme.of(context).textTheme.bodyLarge, + ); + } +} diff --git a/lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart b/lib/pangea/phonetic_transcription/test_phonetic_transcription_api.dart deleted file mode 100644 index e69de29bb..000000000 From daeaf900f3da5eae705e87eb8f6b3255fecf1a31 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:13:40 -0400 Subject: [PATCH 3/4] fix(phonetic_transcription): fixed models --- lib/main.dart | 17 +- .../vocab_analytics_details_view.dart | 37 +++-- .../models/pangea_token_text_model.dart | 8 + .../models/language_model.dart | 27 +++- .../phonetic_transcription_request.dart | 26 +-- .../phonetic_transcription_response.dart | 123 ++++++++++++-- .../phonetic_transcription_widget.dart | 153 ++++++++++++++++++ 7 files changed, 346 insertions(+), 45 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d33e62b41..b7df5a61c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -15,6 +6,14 @@ import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 4f65589cc..c00fb1176 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -1,7 +1,4 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; @@ -15,6 +12,7 @@ import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_ import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; /// Displays information about selected lemma, and its usage class VocabDetailsView extends StatelessWidget { @@ -53,14 +51,33 @@ class VocabDetailsView extends StatelessWidget { : _construct.lemmaCategory.darkColor(context)); return AnalyticsDetailsViewContent( - title: WordTextWithAudioButton( - text: _construct.lemma, - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: textColor, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WordTextWithAudioButton( + text: _construct.lemma, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: textColor, + ), + iconSize: _iconSize, + uniqueID: "${_construct.lemma}-${_construct.category}", + langCode: _userL2!, + ), + if (MatrixState.pangeaController.languageController.userL2 != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: PhoneticTranscriptionWidget( + text: _construct.lemma, + textLanguage: + MatrixState.pangeaController.languageController.userL2!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: textColor.withAlpha((0.7 * 255).toInt()), + fontSize: 18, + ), + iconSize: _iconSize * 0.8, + ), ), - iconSize: _iconSize, - uniqueID: "${_construct.lemma}-${_construct.category}", - langCode: _userL2!, + ], ), subtitle: Column( children: [ diff --git a/lib/pangea/events/models/pangea_token_text_model.dart b/lib/pangea/events/models/pangea_token_text_model.dart index 7f7323cf6..efef9a712 100644 --- a/lib/pangea/events/models/pangea_token_text_model.dart +++ b/lib/pangea/events/models/pangea_token_text_model.dart @@ -22,6 +22,14 @@ class PangeaTokenText { ); } + static PangeaTokenText fromString(String content) { + return PangeaTokenText( + offset: 0, + content: content, + length: content.length, + ); + } + static const String _offsetKey = "offset"; static const String _contentKey = "content"; static const String _lengthKey = "length"; diff --git a/lib/pangea/learning_settings/models/language_model.dart b/lib/pangea/learning_settings/models/language_model.dart index 61f021e91..6d6a03380 100644 --- a/lib/pangea/learning_settings/models/language_model.dart +++ b/lib/pangea/learning_settings/models/language_model.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart'; +import 'package:flutter/material.dart'; class LanguageModel { final String langCode; @@ -80,3 +79,27 @@ class LanguageModel { @override int get hashCode => langCode.hashCode; } + +class LanguageArc { + final LanguageModel l1; + final LanguageModel l2; + + LanguageArc({ + required this.l1, + required this.l2, + }); + + factory LanguageArc.fromJson(Map json) { + return LanguageArc( + l1: LanguageModel.fromJson(json['l1'] as Map), + l2: LanguageModel.fromJson(json['l2'] as Map), + ); + } + + Map toJson() { + return { + 'l1': l1.toJson(), + 'l2': l2.toJson(), + }; + } +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart index 02dfcb7c7..cbd22fc22 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart @@ -1,33 +1,33 @@ +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + class PhoneticTranscriptionRequest { - final String l1; - final String l2; - final String content; + final LanguageArc arc; + final PangeaTokenText content; final bool requiresTokenization; PhoneticTranscriptionRequest({ - required this.l1, - required this.l2, + required this.arc, required this.content, - this.requiresTokenization = true, + this.requiresTokenization = false, }); factory PhoneticTranscriptionRequest.fromJson(Map json) { return PhoneticTranscriptionRequest( - l1: json['l1'] as String, - l2: json['l2'] as String, - content: json['content'] as String, + arc: LanguageArc.fromJson(json['arc'] as Map), + content: + PangeaTokenText.fromJson(json['content'] as Map), requiresTokenization: json['requires_tokenization'] ?? true, ); } Map toJson() { return { - 'l1': l1, - 'l2': l2, - 'content': content, + 'arc': arc.toJson(), + 'content': content.toJson(), 'requires_tokenization': requiresTokenization, }; } - String get storageKey => 'l1:$l1,l2:$l2,content:$content'; + String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}'; } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart index 9efce60d4..a4cd2b3a3 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart @@ -1,8 +1,107 @@ +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + +enum PhoneticTranscriptionDelimEnum { sp, noSp } + +extension PhoneticTranscriptionDelimEnumExt on PhoneticTranscriptionDelimEnum { + String get value { + switch (this) { + case PhoneticTranscriptionDelimEnum.sp: + return " "; + case PhoneticTranscriptionDelimEnum.noSp: + return ""; + } + } + + static PhoneticTranscriptionDelimEnum fromString(String s) { + switch (s) { + case " ": + return PhoneticTranscriptionDelimEnum.sp; + case "": + return PhoneticTranscriptionDelimEnum.noSp; + default: + return PhoneticTranscriptionDelimEnum.sp; + } + } +} + +class PhoneticTranscriptionToken { + final LanguageArc arc; + final PangeaTokenText tokenL2; + final PangeaTokenText phoneticL1Transcription; + + PhoneticTranscriptionToken({ + required this.arc, + required this.tokenL2, + required this.phoneticL1Transcription, + }); + + factory PhoneticTranscriptionToken.fromJson(Map json) { + return PhoneticTranscriptionToken( + arc: LanguageArc.fromJson(json['arc'] as Map), + tokenL2: + PangeaTokenText.fromJson(json['token_l2'] as Map), + phoneticL1Transcription: PangeaTokenText.fromJson( + json['phonetic_l1_transcription'] as Map, + ), + ); + } + + Map toJson() => { + 'arc': arc.toJson(), + 'token_l2': tokenL2.toJson(), + 'phonetic_l1_transcription': phoneticL1Transcription.toJson(), + }; +} + +class PhoneticTranscription { + final LanguageArc arc; + final PangeaTokenText transcriptionL2; + final List phoneticTranscription; + final PhoneticTranscriptionDelimEnum delim; + + PhoneticTranscription({ + required this.arc, + required this.transcriptionL2, + required this.phoneticTranscription, + this.delim = PhoneticTranscriptionDelimEnum.sp, + }); + + factory PhoneticTranscription.fromJson(Map json) { + return PhoneticTranscription( + arc: LanguageArc.fromJson(json['arc'] as Map), + transcriptionL2: PangeaTokenText.fromJson( + json['transcription_l2'] as Map, + ), + phoneticTranscription: (json['phonetic_transcription'] as List) + .map( + (e) => + PhoneticTranscriptionToken.fromJson(e as Map), + ) + .toList(), + delim: json['delim'] != null + ? PhoneticTranscriptionDelimEnumExt.fromString( + json['delim'] as String, + ) + : PhoneticTranscriptionDelimEnum.sp, + ); + } + + Map toJson() => { + 'arc': arc.toJson(), + 'transcription_l2': transcriptionL2.toJson(), + 'phonetic_transcription': + phoneticTranscription.map((e) => e.toJson()).toList(), + 'delim': delim.value, + }; +} + class PhoneticTranscriptionResponse { - final Map arc; - final Map content; - final Map tokenization; - final Map phoneticTranscriptionResult; + final LanguageArc arc; + final PangeaTokenText content; + final Map + tokenization; // You can define a typesafe model if needed + final PhoneticTranscription phoneticTranscriptionResult; DateTime? expireAt; PhoneticTranscriptionResponse({ @@ -15,11 +114,13 @@ class PhoneticTranscriptionResponse { factory PhoneticTranscriptionResponse.fromJson(Map json) { return PhoneticTranscriptionResponse( - arc: Map.from(json['arc'] as Map), - content: Map.from(json['content'] as Map), + arc: LanguageArc.fromJson(json['arc'] as Map), + content: + PangeaTokenText.fromJson(json['content'] as Map), tokenization: Map.from(json['tokenization'] as Map), - phoneticTranscriptionResult: Map.from( - json['phonetic_transcription_result'] as Map), + phoneticTranscriptionResult: PhoneticTranscription.fromJson( + json['phonetic_transcription_result'] as Map, + ), expireAt: json['expireAt'] == null ? null : DateTime.parse(json['expireAt'] as String), @@ -28,10 +129,10 @@ class PhoneticTranscriptionResponse { Map toJson() { return { - 'arc': arc, - 'content': content, + 'arc': arc.toJson(), + 'content': content.toJson(), 'tokenization': tokenization, - 'phonetic_transcription_result': phoneticTranscriptionResult, + 'phonetic_transcription_result': phoneticTranscriptionResult.toJson(), 'expireAt': expireAt?.toIso8601String(), }; } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index b8d7f0d77..abf0c8bc2 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -27,10 +28,51 @@ class PhoneticTranscriptionState extends State { String? error; PhoneticTranscriptionResponse? _response; +======= +import 'dart:async'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +class PhoneticTranscriptionWidget extends StatefulWidget { + final String text; + final LanguageModel textLanguage; + final TextStyle? style; + final double? iconSize; + + const PhoneticTranscriptionWidget({ + super.key, + required this.text, + required this.textLanguage, + this.style, + this.iconSize, + }); + + @override + State createState() => + _PhoneticTranscriptionWidgetState(); +} + +class _PhoneticTranscriptionWidgetState + extends State { + late Future _transcriptionFuture; + bool _hovering = false; + bool _isPlaying = false; + bool _isLoading = false; + late final StreamSubscription _loadingChoreoSubscription; +>>>>>>> Stashed changes @override void initState() { super.initState(); +<<<<<<< Updated upstream _fetchPhoneticTranscription(); } @@ -71,11 +113,69 @@ class PhoneticTranscriptionState extends State { _loading = false; }); } +======= + _transcriptionFuture = _fetchTranscription(); + _loadingChoreoSubscription = + TtsController.loadingChoreoStream.stream.listen((val) { + if (mounted) setState(() => _isLoading = val); + }); + } + + @override + void dispose() { + TtsController.stop(); + _loadingChoreoSubscription.cancel(); + super.dispose(); + } + + Future _fetchTranscription() async { + if (MatrixState.pangeaController.languageController.userL1 == null) { + ErrorHandler.logError( + e: Exception('User L1 is not set'), + data: { + 'text': widget.text, + 'textLanguageCode': widget.textLanguage.langCode, + }, + ); + return widget.text; // Fallback to original text if no L1 is set + } + final req = PhoneticTranscriptionRequest( + arc: LanguageArc( + l1: MatrixState.pangeaController.languageController.userL1!, + l2: widget.textLanguage, + ), + content: PangeaTokenText.fromString(widget.text), + // arc can be omitted for default empty map + ); + final res = await PhoneticTranscriptionRepo.get(req); + return res.phoneticTranscriptionResult.phoneticTranscription.first + .phoneticL1Transcription.content; + } + + Future _handleAudioTap(BuildContext context) async { + if (_isPlaying) { + await TtsController.stop(); + setState(() => _isPlaying = false); + } else { + await TtsController.tryToSpeak( + widget.text, + context: context, + targetID: 'phonetic-transcription-${widget.text}', + langCode: widget.textLanguage.langCode, + onStart: () { + if (mounted) setState(() => _isPlaying = true); + }, + onStop: () { + if (mounted) setState(() => _isPlaying = false); + }, + ); +>>>>>>> Stashed changes } } @override Widget build(BuildContext context) { +<<<<<<< Updated upstream if (error != null) { return Row( mainAxisSize: MainAxisSize.min, @@ -101,6 +201,59 @@ class PhoneticTranscriptionState extends State { return Text( 'Phonetic transcription for "${widget.text}" in ${widget.l2}', style: Theme.of(context).textTheme.bodyLarge, +======= + return FutureBuilder( + future: _transcriptionFuture, + builder: (context, snapshot) { + final transcription = snapshot.data ?? ''; + return MouseRegion( + onEnter: (_) => setState(() => _hovering = true), + onExit: (_) => setState(() => _hovering = false), + child: GestureDetector( + onTap: () => _handleAudioTap(context), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: _hovering + ? Colors.grey.withAlpha((0.2 * 255).round()) + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + transcription.isNotEmpty ? transcription : widget.text, + style: + widget.style ?? Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(width: 8), + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 3), + ) + : Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + size: widget.iconSize ?? 24, + color: _isPlaying + ? Theme.of(context).colorScheme.primary + : Theme.of(context).iconTheme.color, + ), + ), + ], + ), + ), + ), + ); + }, +>>>>>>> Stashed changes ); } } From 7756f2fe9fa4f53324471d1a7107746b8b617fda Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 16 Jun 2025 16:32:13 -0400 Subject: [PATCH 4/4] chore: clean up vocab analytics details popup --- lib/main.dart | 17 +-- .../vocab_analytics_details_view.dart | 30 +++-- .../models/language_model.dart | 3 +- .../phonetic_transcription_widget.dart | 108 +----------------- 4 files changed, 28 insertions(+), 130 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b7df5a61c..d33e62b41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:collection/collection.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -6,14 +15,6 @@ import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index c00fb1176..d472a64b1 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -1,4 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; @@ -9,10 +12,10 @@ import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_icon.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; +import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; /// Displays information about selected lemma, and its usage class VocabDetailsView extends StatelessWidget { @@ -52,16 +55,17 @@ class VocabDetailsView extends StatelessWidget { return AnalyticsDetailsViewContent( title: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - WordTextWithAudioButton( - text: _construct.lemma, - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: textColor, - ), - iconSize: _iconSize, - uniqueID: "${_construct.lemma}-${_construct.category}", - langCode: _userL2!, + LayoutBuilder( + builder: (context, constraints) { + return ShrinkableText( + text: _construct.lemma, + maxWidth: constraints.maxWidth - 40.0, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: textColor, + ), + ); + }, ), if (MatrixState.pangeaController.languageController.userL2 != null) Padding( @@ -81,12 +85,6 @@ class VocabDetailsView extends StatelessWidget { ), subtitle: Column( children: [ - if (_userL1 != null && _userL2 != null) - PhoneticTranscription( - text: _construct.lemma, - l1: _userL1!, - l2: _userL2!, - ), Row( mainAxisSize: MainAxisSize.min, spacing: 8.0, diff --git a/lib/pangea/learning_settings/models/language_model.dart b/lib/pangea/learning_settings/models/language_model.dart index 6d6a03380..1e1ef69b9 100644 --- a/lib/pangea/learning_settings/models/language_model.dart +++ b/lib/pangea/learning_settings/models/language_model.dart @@ -1,7 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart'; -import 'package:flutter/material.dart'; class LanguageModel { final String langCode; diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index abf0c8bc2..d55695dc2 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -1,36 +1,7 @@ -<<<<<<< Updated upstream -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; -import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; -import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; - -class PhoneticTranscription extends StatefulWidget { - final String text; - final String l1; - final String l2; - - const PhoneticTranscription({ - super.key, - required this.text, - required this.l1, - required this.l2, - }); - - @override - State createState() => PhoneticTranscriptionState(); -} - -class PhoneticTranscriptionState extends State { - bool _loading = false; - String? error; - - PhoneticTranscriptionResponse? _response; -======= import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; @@ -39,7 +10,6 @@ import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_ import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; class PhoneticTranscriptionWidget extends StatefulWidget { final String text; @@ -67,53 +37,10 @@ class _PhoneticTranscriptionWidgetState bool _isPlaying = false; bool _isLoading = false; late final StreamSubscription _loadingChoreoSubscription; ->>>>>>> Stashed changes @override void initState() { super.initState(); -<<<<<<< Updated upstream - _fetchPhoneticTranscription(); - } - - @override - void didUpdateWidget(covariant PhoneticTranscription oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.text != widget.text || - oldWidget.l1 != widget.l1 || - oldWidget.l2 != widget.l2) { - _fetchPhoneticTranscription(); - } - } - - Future _fetchPhoneticTranscription() async { - final PhoneticTranscriptionRequest request = PhoneticTranscriptionRequest( - l1: widget.l1, - l2: widget.l2, - content: widget.text, - ); - - try { - setState(() { - _loading = true; - error = null; - }); - - _response = await PhoneticTranscriptionRepo.get(request); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: request.toJson(), - ); - error = e.toString(); - } finally { - if (mounted) { - setState(() { - _loading = false; - }); - } -======= _transcriptionFuture = _fetchTranscription(); _loadingChoreoSubscription = TtsController.loadingChoreoStream.stream.listen((val) { @@ -169,39 +96,11 @@ class _PhoneticTranscriptionWidgetState if (mounted) setState(() => _isPlaying = false); }, ); ->>>>>>> Stashed changes } } @override Widget build(BuildContext context) { -<<<<<<< Updated upstream - if (error != null) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.error_outline, - color: Theme.of(context).colorScheme.error, - size: 16.0, - ), - const SizedBox(width: 8), - Text( - L10n.of(context).phoneticTranscriptionError, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - ); - } - - if (_loading || _response == null) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - return Text( - 'Phonetic transcription for "${widget.text}" in ${widget.l2}', - style: Theme.of(context).textTheme.bodyLarge, -======= return FutureBuilder( future: _transcriptionFuture, builder: (context, snapshot) { @@ -224,7 +123,7 @@ class _PhoneticTranscriptionWidgetState mainAxisSize: MainAxisSize.min, children: [ Text( - transcription.isNotEmpty ? transcription : widget.text, + "/${transcription.isNotEmpty ? transcription : widget.text}/", style: widget.style ?? Theme.of(context).textTheme.bodyMedium, ), @@ -253,7 +152,6 @@ class _PhoneticTranscriptionWidgetState ), ); }, ->>>>>>> Stashed changes ); } }