From 7eaefaba492864cbcfd29a9af91d3bcb42ed9e6b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 9 Dec 2025 12:13:58 -0500 Subject: [PATCH 1/4] fix: use UIARequest for account deletion to enable deletion of SSO accounts --- .../settings_security/settings_security.dart | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index b8aa2584f..e55d4c3c2 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -102,30 +102,47 @@ class SettingsSecurityController extends State { if (mxid == null || mxid.isEmpty || mxid != supposedMxid) { return; } - final input = await showTextInputDialog( - useRootNavigator: false, + // #Pangea + // final input = await showTextInputDialog( + // useRootNavigator: false, + // context: context, + // title: L10n.of(context).pleaseEnterYourPassword, + // okLabel: L10n.of(context).ok, + // cancelLabel: L10n.of(context).cancel, + // isDestructive: true, + // obscureText: true, + // hintText: '******', + // minLines: 1, + // maxLines: 1, + // ); + // if (input == null) return; + // await showFutureLoadingDialog( + // context: context, + // future: () => Matrix.of(context).client.deactivateAccount( + // auth: AuthenticationPassword( + // password: input, + // identifier: AuthenticationUserIdentifier( + // user: Matrix.of(context).client.userID!, + // ), + // ), + // ), + // ); + // Pangea# + + final resp = await showFutureLoadingDialog( context: context, - title: L10n.of(context).pleaseEnterYourPassword, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - isDestructive: true, - obscureText: true, - hintText: '******', - minLines: 1, - maxLines: 1, - ); - if (input == null) return; - await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context).client.deactivateAccount( - auth: AuthenticationPassword( - password: input, - identifier: AuthenticationUserIdentifier( - user: Matrix.of(context).client.userID!, + delay: false, + future: () => + Matrix.of(context).client.uiaRequestBackground( + (auth) => Matrix.of(context).client.deactivateAccount( + auth: auth, + ), ), - ), - ), ); + + if (!resp.isError) { + await Matrix.of(context).client.logout(); + } } void showBootstrapDialog(BuildContext context) async { From c2defa90237fb4f27865a97b99afc63c1585b71c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 9 Dec 2025 14:37:15 -0500 Subject: [PATCH 2/4] fix: always pass lemma info and phonetic transcription in token feedback request --- .../constructs/construct_identifier.dart | 4 +- lib/pangea/lemmas/lemma_info_repo.dart | 7 +- lib/pangea/lemmas/lemma_info_response.dart | 5 + lib/pangea/lemmas/lemma_meaning_builder.dart | 58 ++++--- .../phonetic_transcription_builder.dart | 102 ++++++++++++ .../phonetic_transcription_widget.dart | 149 +++++------------- .../token_info_feedback_dialog.dart | 20 +-- .../token_info_feedback_request.dart | 10 +- .../word_card/lemma_meaning_display.dart | 72 +++++++++ .../word_card/lemma_reaction_picker.dart | 80 ++++++---- .../word_card/reading_assistance_content.dart | 5 +- .../word_card/token_feedback_button.dart | 62 ++++++++ .../toolbar/word_card/word_zoom_widget.dart | 106 ++++--------- 13 files changed, 419 insertions(+), 261 deletions(-) create mode 100644 lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart create mode 100644 lib/pangea/toolbar/word_card/lemma_meaning_display.dart create mode 100644 lib/pangea/toolbar/word_card/token_feedback_button.dart diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index 75878cb74..df4887fbb 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -150,7 +150,7 @@ class ConstructIdentifier { uses: [], ); - LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest( + LemmaInfoRequest get lemmaInfoRequest => LemmaInfoRequest( partOfSpeech: category, lemmaLang: MatrixState.pangeaController.userController.userL2?.langCodeShort ?? @@ -163,7 +163,7 @@ class ConstructIdentifier { /// [lemmmaLang] if not set, assumed to be userL2 Future getLemmaInfo() => LemmaInfoRepo.get( - _lemmaInfoRequest, + lemmaInfoRequest, ); List get userSetEmoji => userLemmaInfo.emojis ?? []; diff --git a/lib/pangea/lemmas/lemma_info_repo.dart b/lib/pangea/lemmas/lemma_info_repo.dart index bdb3d3763..420c50420 100644 --- a/lib/pangea/lemmas/lemma_info_repo.dart +++ b/lib/pangea/lemmas/lemma_info_repo.dart @@ -13,10 +13,13 @@ import 'package:fluffychat/widgets/matrix.dart'; class LemmaInfoRepo { static final GetStorage _lemmaStorage = GetStorage('lemma_storage'); - static void set(LemmaInfoRequest request, LemmaInfoResponse response) { + static Future set( + LemmaInfoRequest request, + LemmaInfoResponse response, + ) async { // set expireAt if not set response.expireAt ??= DateTime.now().add(const Duration(days: 100)); - _lemmaStorage.write(request.storageKey, response.toJson()); + await _lemmaStorage.write(request.storageKey, response.toJson()); } static LemmaInfoResponse? getCached(LemmaInfoRequest request) { diff --git a/lib/pangea/lemmas/lemma_info_response.dart b/lib/pangea/lemmas/lemma_info_response.dart index fb5bca198..ffd5d41bd 100644 --- a/lib/pangea/lemmas/lemma_info_response.dart +++ b/lib/pangea/lemmas/lemma_info_response.dart @@ -22,6 +22,11 @@ class LemmaInfoResponse implements JsonSerializable { ); } + static LemmaInfoResponse get error => LemmaInfoResponse( + emoji: [], + meaning: 'ERROR', + ); + @override Map toJson() { return { diff --git a/lib/pangea/lemmas/lemma_meaning_builder.dart b/lib/pangea/lemmas/lemma_meaning_builder.dart index f1b1560dc..405e8491d 100644 --- a/lib/pangea/lemmas/lemma_meaning_builder.dart +++ b/lib/pangea/lemmas/lemma_meaning_builder.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/languages/language_constants.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart'; @@ -7,6 +8,14 @@ import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/widgets/matrix.dart'; +class _LemmaMeaningLoader extends AsyncLoader { + final LemmaInfoRequest request; + _LemmaMeaningLoader(this.request) : super(); + + @override + Future fetch() => LemmaInfoRepo.get(request); +} + class LemmaMeaningBuilder extends StatefulWidget { final String langCode; final ConstructIdentifier constructId; @@ -27,14 +36,12 @@ class LemmaMeaningBuilder extends StatefulWidget { } class LemmaMeaningBuilderState extends State { - LemmaInfoResponse? lemmaInfo; - bool isLoading = true; - Object? error; + late _LemmaMeaningLoader _loader; @override void initState() { super.initState(); - _fetchLemmaMeaning(); + _reload(); } @override @@ -42,10 +49,25 @@ class LemmaMeaningBuilderState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.constructId != widget.constructId || oldWidget.langCode != widget.langCode) { - _fetchLemmaMeaning(); + _loader.dispose(); + _reload(); } } + @override + void dispose() { + _loader.dispose(); + super.dispose(); + } + + bool get isLoading => _loader.isLoading; + bool get isError => _loader.isError; + + Object? get error => + isError ? (_loader.state.value as AsyncError).error : null; + + LemmaInfoResponse? get lemmaInfo => _loader.value; + LemmaInfoRequest get _request => LemmaInfoRequest( lemma: widget.constructId.lemma, partOfSpeech: widget.constructId.category, @@ -54,27 +76,19 @@ class LemmaMeaningBuilderState extends State { LanguageKeys.defaultLanguage, ); - Future _fetchLemmaMeaning() async { - setState(() { - isLoading = true; - error = null; - }); - - try { - final resp = await LemmaInfoRepo.get(_request); - lemmaInfo = resp; - } catch (e) { - error = e; - } finally { - if (mounted) setState(() => isLoading = false); - } + void _reload() { + _loader = _LemmaMeaningLoader(_request); + _loader.load(); } @override Widget build(BuildContext context) { - return widget.builder( - context, - this, + return ValueListenableBuilder( + valueListenable: _loader.state, + builder: (context, _, __) => widget.builder( + context, + this, + ), ); } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart new file mode 100644 index 000000000..41c25101b --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/common/utils/async_state.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/languages/language_arc_model.dart'; +import 'package:fluffychat/pangea/languages/language_model.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'phonetic_transcription_repo.dart'; + +class _TranscriptLoader extends AsyncLoader { + final PhoneticTranscriptionRequest request; + _TranscriptLoader(this.request) : super(); + + @override + Future fetch() async { + final resp = await PhoneticTranscriptionRepo.get(request); + return resp.phoneticTranscriptionResult.phoneticTranscription.first + .phoneticL1Transcription.content; + } +} + +class PhoneticTranscriptionBuilder extends StatefulWidget { + final LanguageModel textLanguage; + final String text; + + final Widget Function( + BuildContext context, + PhoneticTranscriptionBuilderState controller, + ) builder; + + const PhoneticTranscriptionBuilder({ + super.key, + required this.textLanguage, + required this.text, + required this.builder, + }); + + @override + PhoneticTranscriptionBuilderState createState() => + PhoneticTranscriptionBuilderState(); +} + +class PhoneticTranscriptionBuilderState + extends State { + late _TranscriptLoader _loader; + + @override + void initState() { + super.initState(); + _reload(); + } + + @override + void didUpdateWidget(covariant PhoneticTranscriptionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text || + oldWidget.textLanguage != widget.textLanguage) { + _loader.dispose(); + _reload(); + } + } + + @override + void dispose() { + _loader.dispose(); + super.dispose(); + } + + bool get isLoading => _loader.isLoading; + bool get isError => _loader.isError; + + Object? get error => + isError ? (_loader.state.value as AsyncError).error : null; + + String? get transcription => _loader.value; + + PhoneticTranscriptionRequest get _transcriptRequest => + PhoneticTranscriptionRequest( + arc: LanguageArc( + l1: MatrixState.pangeaController.userController.userL1!, + l2: widget.textLanguage, + ), + content: PangeaTokenText.fromString(widget.text), + ); + + void _reload() { + _loader = _TranscriptLoader(_transcriptRequest); + _loader.load(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _loader.state, + builder: (context, _, __) => widget.builder( + context, + this, + ), + ); + } +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index ae7b533b1..afe549d73 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -4,13 +4,9 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; -import 'package:fluffychat/pangea/languages/language_arc_model.dart'; import 'package:fluffychat/pangea/languages/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/phonetic_transcription/phonetic_transcription_builder.dart'; import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -43,79 +39,6 @@ class PhoneticTranscriptionWidget extends StatefulWidget { class _PhoneticTranscriptionWidgetState extends State { bool _isPlaying = false; - bool _isLoading = false; - Object? _error; - - String? _transcription; - - @override - void initState() { - super.initState(); - _fetchTranscription(); - } - - @override - void didUpdateWidget( - covariant PhoneticTranscriptionWidget oldWidget, - ) { - super.didUpdateWidget(oldWidget); - if (oldWidget.text != widget.text || - oldWidget.textLanguage != widget.textLanguage) { - _fetchTranscription(); - } - } - - Future _fetchTranscription() async { - try { - setState(() { - _isLoading = true; - _error = null; - _transcription = null; - }); - - if (MatrixState.pangeaController.userController.userL1 == null) { - ErrorHandler.logError( - e: Exception('User L1 is not set'), - data: { - 'text': widget.text, - 'textLanguageCode': widget.textLanguage.langCode, - }, - ); - _error = Exception('User L1 is not set'); - return; - } - final req = PhoneticTranscriptionRequest( - arc: LanguageArc( - l1: MatrixState.pangeaController.userController.userL1!, - l2: widget.textLanguage, - ), - content: PangeaTokenText.fromString(widget.text), - // arc can be omitted for default empty map - ); - final res = await PhoneticTranscriptionRepo.get(req); - _transcription = res.phoneticTranscriptionResult.phoneticTranscription - .first.phoneticL1Transcription.content; - } catch (e, s) { - _error = e; - if (e is! UnsubscribedException) { - ErrorHandler.logError( - e: e, - s: s, - data: { - 'text': widget.text, - 'textLanguageCode': widget.textLanguage.langCode, - }, - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - widget.onTranscriptionFetched?.call(); - }); - } - } - } Future _handleAudioTap() async { if (_isPlaying) { @@ -156,14 +79,15 @@ class _PhoneticTranscriptionWidgetState link: MatrixState.pAnyState .layerLinkAndKey("phonetic-transcription-${widget.text}") .link, - child: Row( + child: PhoneticTranscriptionBuilder( key: MatrixState.pAnyState .layerLinkAndKey("phonetic-transcription-${widget.text}") .key, - mainAxisSize: MainAxisSize.min, - children: [ - if (_error != null) - _error is UnsubscribedException + textLanguage: widget.textLanguage, + text: widget.text, + builder: (context, controller) { + if (controller.isError) { + return controller.error is UnsubscribedException ? ErrorIndicator( message: L10n.of(context) .subscribeToUnlockTranscriptions, @@ -176,37 +100,44 @@ class _PhoneticTranscriptionWidgetState : ErrorIndicator( message: L10n.of(context).failedToFetchTranscription, - ) - else if (_isLoading || _transcription == null) - const SizedBox( + ); + } + + if (controller.isLoading || + controller.transcription == null) { + return const SizedBox( width: 16, height: 16, child: CircularProgressIndicator.adaptive(), - ) - else - Flexible( - child: Text( - _transcription!, - textScaler: TextScaler.noScaling, - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, + ); + } + + return Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + controller.transcription!, + textScaler: TextScaler.noScaling, + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + ), ), - ), - if (_transcription != null && _error == null) - const SizedBox(width: 8), - if (_transcription != null && _error == null) - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - size: widget.iconSize ?? 24, - color: widget.iconColor ?? - Theme.of(context).iconTheme.color, + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + size: widget.iconSize ?? 24, + color: widget.iconColor ?? + Theme.of(context).iconTheme.color, + ), ), - ), - ], + ], + ); + }, ), ), ), diff --git a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart index 0660185bf..dbe165a96 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart @@ -8,8 +8,8 @@ import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart' import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/languages/language_arc_model.dart'; import 'package:fluffychat/pangea/languages/p_language_store.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; -import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.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'; @@ -115,19 +115,11 @@ class TokenInfoFeedbackDialog extends StatelessWidget { Future _updateLemmaInfo( PangeaToken token, LemmaInfoResponse response, - ) async { - final construct = token.vocabConstructID; - - final currentLemmaInfo = construct.userLemmaInfo; - final updatedLemmaInfo = UserSetLemmaInfo( - meaning: response.meaning, - emojis: response.emoji, - ); - - if (currentLemmaInfo != updatedLemmaInfo) { - await construct.setUserLemmaInfo(updatedLemmaInfo); - } - } + ) => + LemmaInfoRepo.set( + token.vocabConstructID.lemmaInfoRequest, + response, + ); Future _updatePhoneticTranscription( PhoneticTranscriptionResponse response, diff --git a/lib/pangea/token_info_feedback/token_info_feedback_request.dart b/lib/pangea/token_info_feedback/token_info_feedback_request.dart index 9caf6c4ea..815979493 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_request.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_request.dart @@ -8,8 +8,8 @@ class TokenInfoFeedbackRequestData { final String detectedLanguage; final List tokens; final int selectedToken; - final LemmaInfoResponse? lemmaInfo; - final String? phonetics; + final LemmaInfoResponse lemmaInfo; + final String phonetics; final String wordCardL1; TokenInfoFeedbackRequestData({ @@ -19,8 +19,8 @@ class TokenInfoFeedbackRequestData { required this.detectedLanguage, required this.tokens, required this.selectedToken, - this.lemmaInfo, - this.phonetics, + required this.lemmaInfo, + required this.phonetics, required this.wordCardL1, }); @@ -67,7 +67,7 @@ class TokenInfoFeedbackRequest { 'detected_language': data.detectedLanguage, 'tokens': data.tokens.map((token) => token.toJson()).toList(), 'selected_token': data.selectedToken, - 'lemma_info': data.lemmaInfo?.toJson(), + 'lemma_info': data.lemmaInfo.toJson(), 'phonetics': data.phonetics, 'user_feedback': userFeedback, 'word_card_l1': data.wordCardL1, diff --git a/lib/pangea/toolbar/word_card/lemma_meaning_display.dart b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart new file mode 100644 index 000000000..aac4f1f8d --- /dev/null +++ b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; + +class LemmaMeaningDisplay extends StatelessWidget { + final String langCode; + final ConstructIdentifier constructId; + final String text; + + const LemmaMeaningDisplay({ + super.key, + required this.langCode, + required this.constructId, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return LemmaMeaningBuilder( + langCode: langCode, + constructId: constructId, + builder: (context, controller) { + if (controller.isError) { + return ErrorIndicator( + message: L10n.of(context).errorFetchingDefinition, + style: const TextStyle(fontSize: 14.0), + ); + } + + if (controller.isLoading || controller.lemmaInfo == null) { + return const CircularProgressIndicator.adaptive(); + } + + if (constructId.lemma.toLowerCase() == text.toLowerCase()) { + return Text( + controller.lemmaInfo!.meaning, + style: const TextStyle( + fontSize: 14.0, + ), + textAlign: TextAlign.center, + ); + } + + return RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14.0, + ), + children: [ + TextSpan( + text: constructId.lemma, + ), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + const TextSpan(text: ":"), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + TextSpan( + text: controller.lemmaInfo!.meaning, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart index b4d69b71c..00ba9ee27 100644 --- a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart +++ b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart @@ -4,21 +4,26 @@ import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart'; class LemmaReactionPicker extends StatelessWidget { - final List emojis; - final bool loading; final Event? event; + final ConstructIdentifier construct; + final String langCode; const LemmaReactionPicker({ super.key, - required this.emojis, - required this.loading, - required this.event, + required this.construct, + required this.langCode, + this.event, }); - Future setEmoji(String emoji) async { + Future setEmoji( + String emoji, + List emojis, + ) async { if (event?.room.timeline == null) { throw Exception("Timeline is null in reaction picker"); } @@ -63,33 +68,44 @@ class LemmaReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - final sentReactions = {}; - if (event?.room.timeline != null) { - sentReactions.addAll( - event! - .aggregatedEvents( - event!.room.timeline!, - RelationshipTypes.reaction, - ) - .where( - (event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction', - ) - .map( - (event) => event.content - .tryGetMap('m.relates_to') - ?.tryGet('key'), - ) - .whereType(), - ); - } + return LemmaMeaningBuilder( + langCode: langCode, + constructId: construct, + builder: (context, controller) { + final sentReactions = {}; + if (event?.room.timeline != null) { + sentReactions.addAll( + event! + .aggregatedEvents( + event!.room.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ) + .map( + (event) => event.content + .tryGetMap('m.relates_to') + ?.tryGet('key'), + ) + .whereType(), + ); + } - return LemmaEmojiPicker( - emojis: emojis, - onSelect: event?.room.timeline != null ? setEmoji : null, - disabled: (emoji) => sentReactions.contains(emoji), - loading: loading, + return LemmaEmojiPicker( + emojis: controller.lemmaInfo?.emoji ?? [], + onSelect: event?.room.timeline != null + ? (emoji) => setEmoji( + emoji, + controller.lemmaInfo?.emoji ?? [], + ) + : null, + disabled: (emoji) => sentReactions.contains(emoji), + loading: controller.isLoading, + ); + }, ); } } diff --git a/lib/pangea/toolbar/word_card/reading_assistance_content.dart b/lib/pangea/toolbar/word_card/reading_assistance_content.dart index b607b0d1d..71d2e0d97 100644 --- a/lib/pangea/toolbar/word_card/reading_assistance_content.dart +++ b/lib/pangea/toolbar/word_card/reading_assistance_content.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix_api_lite/model/message_types.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart'; @@ -50,7 +51,7 @@ class ReadingAssistanceContent extends StatelessWidget { onClose: () => overlayController.updateSelectedSpan(null), langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode, onDismissNewWordOverlay: () => overlayController.setState(() {}), - onFlagTokenInfo: () { + onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) { if (selectedTokenIndex < 0) return; final requestData = TokenInfoFeedbackRequestData( userId: Matrix.of(context).client.userID!, @@ -61,6 +62,8 @@ class ReadingAssistanceContent extends StatelessWidget { tokens: tokens ?? [], selectedToken: selectedTokenIndex, wordCardL1: MatrixState.pangeaController.userController.userL1Code!, + lemmaInfo: lemmaInfo, + phonetics: phonetics, ); overlayController.widget.chatController.showTokenFeedbackDialog( requestData, diff --git a/lib/pangea/toolbar/word_card/token_feedback_button.dart b/lib/pangea/toolbar/word_card/token_feedback_button.dart new file mode 100644 index 000000000..dc2a6cff4 --- /dev/null +++ b/lib/pangea/toolbar/word_card/token_feedback_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/languages/language_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart'; + +class TokenFeedbackButton extends StatelessWidget { + final LanguageModel textLanguage; + final ConstructIdentifier constructId; + final String text; + + final Function(LemmaInfoResponse, String) onFlagTokenInfo; + + const TokenFeedbackButton({ + super.key, + required this.textLanguage, + required this.constructId, + required this.text, + required this.onFlagTokenInfo, + }); + + @override + Widget build(BuildContext context) { + return LemmaMeaningBuilder( + langCode: textLanguage.langCode, + constructId: constructId, + builder: (context, lemmaController) { + return PhoneticTranscriptionBuilder( + textLanguage: textLanguage, + text: text, + builder: (context, transcriptController) { + final enabled = (lemmaController.lemmaInfo != null || + lemmaController.isError) && + (transcriptController.transcription != null || + transcriptController.isError); + + final lemmaInfo = + lemmaController.lemmaInfo ?? LemmaInfoResponse.error; + + final transcript = transcriptController.transcription ?? 'ERROR'; + + return IconButton( + icon: const Icon(Icons.flag_outlined), + onPressed: enabled + ? () { + onFlagTokenInfo( + lemmaInfo, + transcript, + ); + } + : null, + tooltip: enabled ? L10n.of(context).reportWordIssueTooltip : null, + ); + }, + ); + }, + ); + } +} diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart index d88a4e787..3bb8b0bb9 100644 --- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart +++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart @@ -3,18 +3,18 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/languages/language_model.dart'; import 'package:fluffychat/pangea/languages/p_language_store.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/new_word_overlay.dart'; +import 'package:fluffychat/pangea/toolbar/word_card/lemma_meaning_display.dart'; import 'package:fluffychat/pangea/toolbar/word_card/lemma_reaction_picker.dart'; import 'package:fluffychat/pangea/toolbar/word_card/message_unsubscribed_card.dart'; +import 'package:fluffychat/pangea/toolbar/word_card/token_feedback_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; class WordZoomWidget extends StatelessWidget { @@ -28,10 +28,7 @@ class WordZoomWidget extends StatelessWidget { final Event? event; final VoidCallback? onDismissNewWordOverlay; - final VoidCallback? onFlagTokenInfo; - - // final TokenInfoFeedbackRequestData? requestData; - // final PangeaMessageEvent? pangeaMessageEvent; + final Function(LemmaInfoResponse, String)? onFlagTokenInfo; const WordZoomWidget({ super.key, @@ -55,6 +52,8 @@ class WordZoomWidget extends StatelessWidget { final bool? subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; final overlayColor = Theme.of(context).scaffoldBackgroundColor; + final showTranscript = + MatrixState.pangeaController.userController.showTranscription; final Widget content = subscribed != null && !subscribed ? const MessageUnsubscribedCard() @@ -106,11 +105,14 @@ class WordZoomWidget extends StatelessWidget { ), ), onFlagTokenInfo != null - ? IconButton( - icon: const Icon(Icons.flag_outlined), - onPressed: onFlagTokenInfo, - tooltip: - L10n.of(context).reportWordIssueTooltip, + ? TokenFeedbackButton( + textLanguage: PLanguageStore.byLangCode( + langCode, + ) ?? + LanguageModel.unknown, + constructId: construct, + text: token.content, + onFlagTokenInfo: onFlagTokenInfo!, ) : const SizedBox( width: 40.0, @@ -118,17 +120,12 @@ class WordZoomWidget extends StatelessWidget { ), ], ), - LemmaMeaningBuilder( - langCode: langCode, - constructId: construct, - builder: (context, controller) { - return Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - if (MatrixState.pangeaController.userController - .showTranscription) - PhoneticTranscriptionWidget( + Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + showTranscript + ? PhoneticTranscriptionWidget( text: token.content, textLanguage: PLanguageStore.byLangCode( langCode, @@ -137,62 +134,23 @@ class WordZoomWidget extends StatelessWidget { style: const TextStyle(fontSize: 14.0), iconSize: 24.0, ) - else - WordAudioButton( + : WordAudioButton( text: token.content, uniqueID: "lemma-content-${token.content}", langCode: langCode, iconSize: 24.0, ), - LemmaReactionPicker( - emojis: controller.lemmaInfo?.emoji ?? [], - loading: controller.isLoading, - event: event, - ), - if (controller.error != null) - ErrorIndicator( - message: L10n.of(context) - .errorFetchingDefinition, - style: const TextStyle(fontSize: 14.0), - ) - else if (controller.isLoading || - controller.lemmaInfo == null) - const CircularProgressIndicator.adaptive() - else - construct.lemma.toLowerCase() == - token.content.toLowerCase() - ? Text( - controller.lemmaInfo!.meaning, - style: - const TextStyle(fontSize: 14.0), - textAlign: TextAlign.center, - ) - : RichText( - text: TextSpan( - style: DefaultTextStyle.of(context) - .style - .copyWith( - fontSize: 14.0, - ), - children: [ - TextSpan(text: construct.lemma), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - const TextSpan(text: ":"), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - TextSpan( - text: controller - .lemmaInfo!.meaning, - ), - ], - ), - ), - ], - ); - }, + LemmaReactionPicker( + construct: construct, + langCode: langCode, + event: event, + ), + LemmaMeaningDisplay( + langCode: langCode, + constructId: construct, + text: token.content, + ), + ], ), ], ), From 84737dbca1b9e63523a2e300a2af31104656677a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 9 Dec 2025 16:20:17 -0500 Subject: [PATCH 3/4] update lemma meaning and phonetic transcription repos --- lib/pangea/common/utils/async_state.dart | 6 + .../constructs/construct_identifier.dart | 4 +- lib/pangea/languages/language_arc_model.dart | 8 + lib/pangea/lemmas/lemma_info_repo.dart | 165 ++++++++++++++---- lib/pangea/lemmas/lemma_info_response.dart | 6 - lib/pangea/lemmas/lemma_meaning_builder.dart | 13 +- .../phonetic_transcription_builder.dart | 14 +- .../phonetic_transcription_repo.dart | 157 +++++++++++++---- .../phonetic_transcription_request.dart | 12 ++ .../phonetic_transcription_response.dart | 6 - .../emoji_activity_generator.dart | 17 +- .../lemma_meaning_activity_generator.dart | 14 +- .../practice_generation_repo.dart | 3 + 13 files changed, 337 insertions(+), 88 deletions(-) diff --git a/lib/pangea/common/utils/async_state.dart b/lib/pangea/common/utils/async_state.dart index a9fd60cb5..f14b88a61 100644 --- a/lib/pangea/common/utils/async_state.dart +++ b/lib/pangea/common/utils/async_state.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -88,6 +90,10 @@ abstract class AsyncLoader { final result = await fetch(); if (_disposed) return; state.value = AsyncState.loaded(result); + } on HttpException catch (e) { + if (!_disposed) { + state.value = AsyncState.error(e); + } } catch (e, s) { ErrorHandler.logError( e: e, diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index df4887fbb..aa980be0a 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -162,7 +163,8 @@ class ConstructIdentifier { ); /// [lemmmaLang] if not set, assumed to be userL2 - Future getLemmaInfo() => LemmaInfoRepo.get( + Future> getLemmaInfo() => LemmaInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, lemmaInfoRequest, ); diff --git a/lib/pangea/languages/language_arc_model.dart b/lib/pangea/languages/language_arc_model.dart index 3e450c9c2..c55d8a66b 100644 --- a/lib/pangea/languages/language_arc_model.dart +++ b/lib/pangea/languages/language_arc_model.dart @@ -22,4 +22,12 @@ class LanguageArc { 'l2': l2.toJson(), }; } + + @override + int get hashCode => l1.hashCode ^ l2.hashCode; + + @override + bool operator ==(Object other) { + return other is LanguageArc && other.l1 == l1 && other.l2 == l2; + } } diff --git a/lib/pangea/lemmas/lemma_info_repo.dart b/lib/pangea/lemmas/lemma_info_repo.dart index 420c50420..02d6712ef 100644 --- a/lib/pangea/lemmas/lemma_info_repo.dart +++ b/lib/pangea/lemmas/lemma_info_repo.dart @@ -1,51 +1,105 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:async/async.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'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; -import 'package:fluffychat/widgets/matrix.dart'; + +class _LemmaInfoCacheItem { + final Future> resultFuture; + final DateTime timestamp; + + const _LemmaInfoCacheItem({ + required this.resultFuture, + required this.timestamp, + }); +} class LemmaInfoRepo { - static final GetStorage _lemmaStorage = GetStorage('lemma_storage'); + // In-memory cache + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); + + // Persistent storage + static final GetStorage _storage = GetStorage('lemma_storage'); + + /// Public entry point + static Future> get( + String accessToken, + LemmaInfoRequest request, + ) { + // 1. Try memory cache + final cached = _getCached(request); + if (cached != null) { + return cached; + } + + // 2. Try disk cache + final stored = _getStored(request); + if (stored != null) { + return Future.value(Result.value(stored)); + } + + // 3. Fetch from network (safe future) + final future = _safeFetch(accessToken, request); + + // 4. Save to in-memory cache + _cache[request.hashCode.toString()] = _LemmaInfoCacheItem( + resultFuture: future, + timestamp: DateTime.now(), + ); + + // 5. Write to disk *after* the fetch finishes, without rethrowing + writeToDisk(request, future); + + return future; + } static Future set( LemmaInfoRequest request, - LemmaInfoResponse response, + LemmaInfoResponse resultFuture, ) async { - // set expireAt if not set - response.expireAt ??= DateTime.now().add(const Duration(days: 100)); - await _lemmaStorage.write(request.storageKey, response.toJson()); - } - - static LemmaInfoResponse? getCached(LemmaInfoRequest request) { - final cachedJson = _lemmaStorage.read(request.storageKey); - - final cached = - cachedJson == null ? null : LemmaInfoResponse.fromJson(cachedJson); - - if (cached != null) { - if (DateTime.now().isBefore(cached.expireAt!)) { - return cached; - } else { - _lemmaStorage.remove(request.storageKey); - } + final key = request.hashCode.toString(); + try { + await _storage.write(key, resultFuture.toJson()); + _cache.remove(key); // Invalidate in-memory cache + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'lemma': request.lemma}, + ); } - return null; } - /// Get lemma info, prefering user set data over fetched data - static Future get(LemmaInfoRequest request) async { - final cached = getCached(request); - if (cached != null) return cached; + static Future> _safeFetch( + String token, + LemmaInfoRequest request, + ) async { + try { + final resp = await _fetch(token, request); + return Result.value(resp); + } catch (e, s) { + // Ensure error is logged and converted to a Result + ErrorHandler.logError(e: e, s: s, data: request.toJson()); + return Result.error(e); + } + } - final Requests req = Requests( + static Future _fetch( + String accessToken, + LemmaInfoRequest request, + ) async { + final req = Requests( choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, + accessToken: accessToken, ); final Response res = await req.post( @@ -53,10 +107,59 @@ class LemmaInfoRepo { body: request.toJson(), ); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = LemmaInfoResponse.fromJson(decodedBody); + if (res.statusCode != 200) { + throw HttpException( + 'Failed to fetch lemma info: ${res.statusCode} ${res.reasonPhrase}', + ); + } - set(request, response); - return response; + return LemmaInfoResponse.fromJson( + jsonDecode(utf8.decode(res.bodyBytes)), + ); + } + + static Future>? _getCached( + LemmaInfoRequest request, + ) { + final now = DateTime.now(); + final key = request.hashCode.toString(); + + // Remove stale entries first + _cache.removeWhere( + (_, item) => now.difference(item.timestamp) >= _cacheDuration, + ); + + final item = _cache[key]; + return item?.resultFuture; + } + + static Future writeToDisk( + LemmaInfoRequest request, + Future> resultFuture, + ) async { + final result = await resultFuture; // SAFE: never throws + + if (!result.isValue) return; // only cache successful responses + await set(request, result.asValue!.value); + } + + static LemmaInfoResponse? _getStored( + LemmaInfoRequest request, + ) { + final key = request.hashCode.toString(); + try { + final entry = _storage.read(key); + if (entry == null) return null; + + return LemmaInfoResponse.fromJson(entry); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'lemma': request.lemma}, + ); + _storage.remove(key); + return null; + } } } diff --git a/lib/pangea/lemmas/lemma_info_response.dart b/lib/pangea/lemmas/lemma_info_response.dart index ffd5d41bd..6edec1fc7 100644 --- a/lib/pangea/lemmas/lemma_info_response.dart +++ b/lib/pangea/lemmas/lemma_info_response.dart @@ -3,12 +3,10 @@ import 'package:fluffychat/pangea/events/models/content_feedback.dart'; class LemmaInfoResponse implements JsonSerializable { final List emoji; final String meaning; - DateTime? expireAt; LemmaInfoResponse({ required this.emoji, required this.meaning, - this.expireAt, }); factory LemmaInfoResponse.fromJson(Map json) { @@ -16,9 +14,6 @@ class LemmaInfoResponse implements JsonSerializable { // NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis emoji: (json['emoji'] as List).map((e) => e as String).toList(), meaning: json['meaning'] as String, - expireAt: json['expireAt'] == null - ? null - : DateTime.parse(json['expireAt'] as String), ); } @@ -32,7 +27,6 @@ class LemmaInfoResponse implements JsonSerializable { return { 'emoji': emoji, 'meaning': meaning, - 'expireAt': expireAt?.toIso8601String(), }; } diff --git a/lib/pangea/lemmas/lemma_meaning_builder.dart b/lib/pangea/lemmas/lemma_meaning_builder.dart index 405e8491d..30603329c 100644 --- a/lib/pangea/lemmas/lemma_meaning_builder.dart +++ b/lib/pangea/lemmas/lemma_meaning_builder.dart @@ -13,7 +13,18 @@ class _LemmaMeaningLoader extends AsyncLoader { _LemmaMeaningLoader(this.request) : super(); @override - Future fetch() => LemmaInfoRepo.get(request); + Future fetch() async { + final result = await LemmaInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, + request, + ); + + if (result.isError) { + throw result.asError!.error; + } + + return result.asValue!.value; + } } class LemmaMeaningBuilder extends StatefulWidget { diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart index 41c25101b..366f9b1fe 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart @@ -14,9 +14,17 @@ class _TranscriptLoader extends AsyncLoader { @override Future fetch() async { - final resp = await PhoneticTranscriptionRepo.get(request); - return resp.phoneticTranscriptionResult.phoneticTranscription.first - .phoneticL1Transcription.content; + final resp = await PhoneticTranscriptionRepo.get( + MatrixState.pangeaController.userController.accessToken, + request, + ); + + if (resp.isError) { + throw resp.asError!.error; + } + + return resp.asValue!.value.phoneticTranscriptionResult.phoneticTranscription + .first.phoneticL1Transcription.content; } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart index 7fe6885fe..33760bc9a 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -1,8 +1,7 @@ import 'dart:convert'; -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; +import 'dart:io'; +import 'package:async/async.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart'; @@ -12,39 +11,95 @@ 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'; + +class _PhoneticTranscriptionCacheItem { + final Future> resultFuture; + final DateTime timestamp; + + const _PhoneticTranscriptionCacheItem({ + required this.resultFuture, + required this.timestamp, + }); +} class PhoneticTranscriptionRepo { + // In-memory cache + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); + +// Persistent storage static final GetStorage _storage = GetStorage('phonetic_transcription_storage'); + static Future> get( + String accessToken, + PhoneticTranscriptionRequest request, + ) { + // 1. Try memory cache + final cached = _getCached(request); + if (cached != null) { + return cached; + } + + // 2. Try disk cache + final stored = _getStored(request); + if (stored != null) { + return Future.value(Result.value(stored)); + } + + // 3. Fetch from network (safe future) + final future = _safeFetch(accessToken, request); + + // 4. Save to in-memory cache + _cache[request.hashCode.toString()] = _PhoneticTranscriptionCacheItem( + resultFuture: future, + timestamp: DateTime.now(), + ); + + // 5. Write to disk *after* the fetch finishes, without rethrowing + writeToDisk(request, future); + + return future; + } + static Future set( PhoneticTranscriptionRequest request, - PhoneticTranscriptionResponse response, + PhoneticTranscriptionResponse resultFuture, ) async { - response.expireAt ??= DateTime.now().add(const Duration(days: 100)); - await _storage.write(request.storageKey, response.toJson()); + final key = request.hashCode.toString(); + try { + await _storage.write(key, resultFuture.toJson()); + _cache.remove(key); // Invalidate in-memory cache + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'request': request.toJson()}, + ); + } + } + + static Future> _safeFetch( + String token, + PhoneticTranscriptionRequest request, + ) async { + try { + final resp = await _fetch(token, request); + return Result.value(resp); + } catch (e, s) { + // Ensure error is logged and converted to a Result + ErrorHandler.logError(e: e, s: s, data: request.toJson()); + return Result.error(e); + } } static Future _fetch( + String accessToken, 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( + final req = Requests( choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, + accessToken: accessToken, ); final Response res = await req.post( @@ -52,21 +107,59 @@ class PhoneticTranscriptionRepo { body: request.toJson(), ); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = PhoneticTranscriptionResponse.fromJson(decodedBody); - set(request, response); - return response; + if (res.statusCode != 200) { + throw HttpException( + 'Failed to fetch phonetic transcription: ${res.statusCode} ${res.reasonPhrase}', + ); + } + + return PhoneticTranscriptionResponse.fromJson( + jsonDecode(utf8.decode(res.bodyBytes)), + ); } - static Future get( + static Future>? _getCached( PhoneticTranscriptionRequest request, + ) { + final now = DateTime.now(); + final key = request.hashCode.toString(); + + // Remove stale entries first + _cache.removeWhere( + (_, item) => now.difference(item.timestamp) >= _cacheDuration, + ); + + final item = _cache[key]; + return item?.resultFuture; + } + + static Future writeToDisk( + PhoneticTranscriptionRequest request, + Future> resultFuture, ) async { + final result = await resultFuture; // SAFE: never throws + + if (!result.isValue) return; // only cache successful responses + await set(request, result.asValue!.value); + } + + static PhoneticTranscriptionResponse? _getStored( + PhoneticTranscriptionRequest request, + ) { + final key = request.hashCode.toString(); try { - return await _fetch(request); - } catch (e) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, data: request.toJson()); - rethrow; + final entry = _storage.read(key); + if (entry == null) return null; + + return PhoneticTranscriptionResponse.fromJson(entry); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'request': request.toJson()}, + ); + _storage.remove(key); + return null; } } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart index be229cba1..464193b01 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart @@ -30,4 +30,16 @@ class PhoneticTranscriptionRequest { } String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}'; + + @override + int get hashCode => + content.hashCode ^ arc.hashCode ^ requiresTokenization.hashCode; + + @override + bool operator ==(Object other) { + return other is PhoneticTranscriptionRequest && + other.content == content && + other.arc == arc && + other.requiresTokenization == requiresTokenization; + } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart index 0f9a62b43..612ef42c1 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart @@ -102,14 +102,12 @@ class PhoneticTranscriptionResponse { final Map tokenization; // You can define a typesafe model if needed final PhoneticTranscription phoneticTranscriptionResult; - DateTime? expireAt; PhoneticTranscriptionResponse({ required this.arc, required this.content, required this.tokenization, required this.phoneticTranscriptionResult, - this.expireAt, }); factory PhoneticTranscriptionResponse.fromJson(Map json) { @@ -121,9 +119,6 @@ class PhoneticTranscriptionResponse { phoneticTranscriptionResult: PhoneticTranscription.fromJson( json['phonetic_transcription_result'] as Map, ), - expireAt: json['expireAt'] == null - ? null - : DateTime.parse(json['expireAt'] as String), ); } @@ -133,7 +128,6 @@ class PhoneticTranscriptionResponse { 'content': content.toJson(), 'tokenization': tokenization, 'phonetic_transcription_result': phoneticTranscriptionResult.toJson(), - 'expireAt': expireAt?.toIso8601String(), }; } diff --git a/lib/pangea/practice_activities/emoji_activity_generator.dart b/lib/pangea/practice_activities/emoji_activity_generator.dart index 3354421c5..3cb5584bd 100644 --- a/lib/pangea/practice_activities/emoji_activity_generator.dart +++ b/lib/pangea/practice_activities/emoji_activity_generator.dart @@ -1,3 +1,5 @@ +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; @@ -35,15 +37,20 @@ class EmojiActivityGenerator { } } - final List> lemmaInfoFutures = missingEmojis - .map((token) => token.vocabConstructID.getLemmaInfo()) - .toList(); + final List>> lemmaInfoFutures = + missingEmojis + .map((token) => token.vocabConstructID.getLemmaInfo()) + .toList(); - final List lemmaInfos = + final List> lemmaInfos = await Future.wait(lemmaInfoFutures); for (int i = 0; i < missingEmojis.length; i++) { - final e = lemmaInfos[i].emoji.firstWhere( + if (lemmaInfos[i].isError) { + throw lemmaInfos[i].asError!.error; + } + + final e = lemmaInfos[i].asValue!.value.emoji.firstWhere( (e) => !usedEmojis.contains(e), orElse: () => throw Exception( "Not enough unique emojis for tokens in message", diff --git a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart index dfe8515d9..02d839940 100644 --- a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart +++ b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart @@ -1,26 +1,34 @@ import 'dart:async'; +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_match.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class LemmaMeaningActivityGenerator { static Future get( MessageActivityRequest req, ) async { - final List> lemmaInfoFutures = req.targetTokens + final List>> lemmaInfoFutures = req + .targetTokens .map((token) => token.vocabConstructID.getLemmaInfo()) .toList(); - final List lemmaInfos = + final List> lemmaInfos = await Future.wait(lemmaInfoFutures); + if (lemmaInfos.any((result) => result.isError)) { + throw lemmaInfos.firstWhere((result) => result.isError).error!; + } + final Map> matchInfo = Map.fromIterables( req.targetTokens.map((token) => token.vocabForm), - lemmaInfos.map((lemmaInfo) => [lemmaInfo.meaning]), + lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]), ); return MessageActivityResponse( diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 008461d2e..423f4033a 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -69,6 +70,8 @@ class PracticeRepo { _setCached(req, res); return Result.value(res.activity); + } on HttpException catch (e, s) { + return Result.error(e, s); } catch (e, s) { ErrorHandler.logError( e: e, From 43014b3207ec56f02b8225c340b7d65b78c0bddd Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 10 Dec 2025 16:05:14 -0500 Subject: [PATCH 4/4] chore: simplify progress bar widget --- .../level_up/level_popup_progess_bar.dart | 43 +++++++ .../level_up/level_up_popup.dart | 29 +---- .../learning_progress_bar.dart | 21 ++-- .../progress_bar/animated_level_dart.dart | 105 ------------------ .../progress_bar/animated_progress_bar.dart | 62 +++++++++++ .../progress_bar/level_bar.dart | 43 ------- .../progress_bar/progress_bar.dart | 64 ----------- .../progress_bar/progress_bar_background.dart | 30 ----- .../progress_bar/progress_bar_details.dart | 25 ----- 9 files changed, 118 insertions(+), 304 deletions(-) create mode 100644 lib/pangea/analytics_misc/level_up/level_popup_progess_bar.dart delete mode 100644 lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart create mode 100644 lib/pangea/analytics_summary/progress_bar/animated_progress_bar.dart delete mode 100644 lib/pangea/analytics_summary/progress_bar/level_bar.dart delete mode 100644 lib/pangea/analytics_summary/progress_bar/progress_bar.dart delete mode 100644 lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart delete mode 100644 lib/pangea/analytics_summary/progress_bar/progress_bar_details.dart diff --git a/lib/pangea/analytics_misc/level_up/level_popup_progess_bar.dart b/lib/pangea/analytics_misc/level_up/level_popup_progess_bar.dart new file mode 100644 index 000000000..a7dbb34ce --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_popup_progess_bar.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_progress_bar.dart'; + +class LevelPopupProgressBar extends StatefulWidget { + final double height; + final Duration duration; + + const LevelPopupProgressBar({ + required this.height, + required this.duration, + super.key, + }); + + @override + LevelPopupProgressBarState createState() => LevelPopupProgressBarState(); +} + +class LevelPopupProgressBarState extends State { + double width = 0.0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + width = 1.0; + }); + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedProgressBar( + height: widget.height, + widthPercent: width, + barColor: AppConfig.goldLight, + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + duration: widget.duration, + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index 53a68d397..8795ab461 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -12,11 +12,10 @@ import 'package:matrix/matrix_api_lite/generated/model.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_popup_progess_bar.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; @@ -193,11 +192,6 @@ class _LevelUpPopupContentState extends State @override @override Widget build(BuildContext context) { - final Animation progressAnimation = - Tween(begin: 0, end: 1).animate( - CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)), - ); - final Animation vocabAnimation = IntTween(begin: _startVocab, end: _endVocab).animate( CurvedAnimation( @@ -282,23 +276,10 @@ class _LevelUpPopupContentState extends State animation: _controller, builder: (_, __) => Row( children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return LevelBar( - details: const LevelBarDetails( - fillColor: AppConfig.goldLight, - currentPoints: 0, - widthMultiplier: 1, - ), - progressBarDetails: ProgressBarDetails( - totalWidth: constraints.maxWidth * - progressAnimation.value, - height: 20, - borderColor: colorScheme.primary, - ), - ); - }, + const Expanded( + child: LevelPopupProgressBar( + height: 20, + duration: Duration(milliseconds: 1000), ), ), const SizedBox(width: 8), diff --git a/lib/pangea/analytics_summary/learning_progress_bar.dart b/lib/pangea/analytics_summary/learning_progress_bar.dart index b9f413520..7ee7bee2d 100644 --- a/lib/pangea/analytics_summary/learning_progress_bar.dart +++ b/lib/pangea/analytics_summary/learning_progress_bar.dart @@ -1,21 +1,20 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_progress_bar.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LearningProgressBar extends StatelessWidget { final int level; final int totalXP; - final double? height; + final double height; final bool loading; const LearningProgressBar({ required this.level, required this.totalXP, required this.loading, - this.height, + required this.height, super.key, }); @@ -30,16 +29,12 @@ class LearningProgressBar extends StatelessWidget { ), ); } - return ProgressBar( + + return AnimatedProgressBar( height: height, - levelBars: [ - LevelBarDetails( - fillColor: Theme.of(context).colorScheme.primary, - currentPoints: totalXP, - widthMultiplier: - MatrixState.pangeaController.getAnalytics.levelProgress, - ), - ], + widthPercent: MatrixState.pangeaController.getAnalytics.levelProgress, + barColor: AppConfig.goldLight, + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, ); } } diff --git a/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart b/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart deleted file mode 100644 index ccc966528..000000000 --- a/lib/pangea/analytics_summary/progress_bar/animated_level_dart.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; - -class AnimatedLevelBar extends StatefulWidget { - final double height; - final double beginWidth; - final double endWidth; - final Color primaryColor; - - const AnimatedLevelBar({ - super.key, - required this.height, - required this.beginWidth, - required this.endWidth, - required this.primaryColor, - }); - - @override - AnimatedLevelBarState createState() => AnimatedLevelBarState(); -} - -class AnimatedLevelBarState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - double get _beginWidth => - widget.beginWidth == 0 ? 0 : max(20, widget.beginWidth); - double get _endWidth => widget.endWidth == 0 ? 0 : max(20, widget.endWidth); - - /// Whether the animation has run for the first time during initState. Don't - /// want the animation to run when the widget mounts, only when points are gained. - bool _init = true; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _controller.forward().then((_) => _init = false); - } - - @override - void didUpdateWidget(covariant AnimatedLevelBar oldWidget) { - super.didUpdateWidget(oldWidget); - if ((oldWidget.endWidth == 0 ? 0 : max(20, oldWidget.endWidth)) != - (widget.endWidth == 0 ? 0 : max(20, widget.endWidth))) { - _controller.reset(); - _controller.forward(); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Animation get _animation { - // If this is the first run of the animation, don't animate. This is just the widget mounting, - // not a points gain. This could instead be 'if going from 0 to a non-zero value', but that - // would remove the animation for first points gained. It would remove the need for a flag though. - if (_init) { - return Tween( - begin: _endWidth, - end: _endWidth, - ).animate(_controller); - } - - // animate the width of the bar - return Tween( - begin: _beginWidth, - end: _endWidth, - ).animate(_controller); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Stack( - children: [ - Container( - height: widget.height, - width: _animation.value, - decoration: BoxDecoration( - color: widget.primaryColor, - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/pangea/analytics_summary/progress_bar/animated_progress_bar.dart b/lib/pangea/analytics_summary/progress_bar/animated_progress_bar.dart new file mode 100644 index 000000000..94bf85c27 --- /dev/null +++ b/lib/pangea/analytics_summary/progress_bar/animated_progress_bar.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; + +class AnimatedProgressBar extends StatelessWidget { + final double height; + final double widthPercent; + + final Color barColor; + final Color backgroundColor; + final Duration? duration; + + const AnimatedProgressBar({ + required this.height, + required this.widthPercent, + required this.barColor, + required this.backgroundColor, + this.duration, + super.key, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Container( + height: height, + width: constraints.maxWidth, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + color: backgroundColor, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: AnimatedContainer( + duration: duration ?? FluffyThemes.animationDuration, + height: height, + width: constraints.maxWidth * widthPercent, + decoration: BoxDecoration( + color: barColor, + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/pangea/analytics_summary/progress_bar/level_bar.dart b/lib/pangea/analytics_summary/progress_bar/level_bar.dart deleted file mode 100644 index 3bf56c325..000000000 --- a/lib/pangea/analytics_summary/progress_bar/level_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/animated_level_dart.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; - -class LevelBar extends StatefulWidget { - final LevelBarDetails details; - final ProgressBarDetails progressBarDetails; - - const LevelBar({ - super.key, - required this.details, - required this.progressBarDetails, - }); - - @override - LevelBarState createState() => LevelBarState(); -} - -class LevelBarState extends State { - double prevWidth = 0; - double get width => - widget.progressBarDetails.totalWidth * widget.details.widthMultiplier; - - @override - void didUpdateWidget(covariant LevelBar oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.details.currentPoints != widget.details.currentPoints) { - setState(() => prevWidth = width); - } - } - - @override - Widget build(BuildContext context) { - return AnimatedLevelBar( - height: widget.progressBarDetails.height, - beginWidth: prevWidth, - endWidth: width, - primaryColor: AppConfig.goldLight, - ); - } -} diff --git a/lib/pangea/analytics_summary/progress_bar/progress_bar.dart b/lib/pangea/analytics_summary/progress_bar/progress_bar.dart deleted file mode 100644 index a257c1a21..000000000 --- a/lib/pangea/analytics_summary/progress_bar/progress_bar.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_background.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; - -// Provide an order list of level indicators, each with it's color -// and stream. Also provide an overall width and pointsPerLevel. - -class ProgressBar extends StatefulWidget { - final List levelBars; - final double? height; - - const ProgressBar({ - super.key, - required this.levelBars, - this.height, - }); - - @override - ProgressBarState createState() => ProgressBarState(); -} - -class ProgressBarState extends State { - double width = 0; - void setWidth(double newWidth) { - if (width != newWidth) { - setState(() => width = newWidth); - } - } - - get progressBarDetails => ProgressBarDetails( - totalWidth: width, - borderColor: Theme.of(context).colorScheme.secondaryContainer, - height: widget.height ?? 14, - ); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - if (width != constraints.maxWidth) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => setWidth(constraints.maxWidth), - ); - } - return Stack( - alignment: Alignment.centerLeft, - children: [ - ProgressBarBackground(details: progressBarDetails), - for (final levelBar in widget.levelBars) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: LevelBar( - details: levelBar, - progressBarDetails: progressBarDetails, - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart b/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart deleted file mode 100644 index a3e320d07..000000000 --- a/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; - -class ProgressBarBackground extends StatelessWidget { - final ProgressBarDetails details; - - const ProgressBarBackground({ - super.key, - required this.details, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: Container( - height: details.height, - width: details.totalWidth, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), - color: details.borderColor, - ), - ), - ); - } -} diff --git a/lib/pangea/analytics_summary/progress_bar/progress_bar_details.dart b/lib/pangea/analytics_summary/progress_bar/progress_bar_details.dart deleted file mode 100644 index a5196bf65..000000000 --- a/lib/pangea/analytics_summary/progress_bar/progress_bar_details.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:ui'; - -class LevelBarDetails { - final Color fillColor; - final int currentPoints; - final double widthMultiplier; - - const LevelBarDetails({ - required this.fillColor, - required this.currentPoints, - required this.widthMultiplier, - }); -} - -class ProgressBarDetails { - final double totalWidth; - final Color borderColor; - final double height; - - const ProgressBarDetails({ - required this.totalWidth, - required this.borderColor, - this.height = 14, - }); -}