From 3d85d2ec9f8f65a13326c34ac38f16d3ab6b7990 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 20 Jan 2025 14:02:56 -0500 Subject: [PATCH] feat: widget for customizing SVG colors (#1498) * feat: widget for customizing SVG colors * feat: replace morph icons with customized morph SVGs --- lib/pangea/analytics/utils/get_svg_link.dart | 184 ++++++++++++++++++ lib/pangea/common/widgets/customized_svg.dart | 67 +++++++ .../morphs/morphological_list_item.dart | 49 ++++- .../morphs/morphological_list_widget.dart | 9 +- 4 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 lib/pangea/analytics/utils/get_svg_link.dart create mode 100644 lib/pangea/common/widgets/customized_svg.dart diff --git a/lib/pangea/analytics/utils/get_svg_link.dart b/lib/pangea/analytics/utils/get_svg_link.dart new file mode 100644 index 000000000..ad3dd9376 --- /dev/null +++ b/lib/pangea/analytics/utils/get_svg_link.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; + +String? getMorphSvgLink({ + required String morphFeature, + String? morphTag, + required BuildContext context, +}) { + const baseURL = + "https://pangea-chat-client-assets.s3.us-east-1.amazonaws.com"; + + if (morphTag == null) { + final key = morphFeature.toLowerCase(); + String? filename; + switch (key) { + case "advtype": + filename = "AdverbType.svg"; + case "aspect": + filename = "Aspect.svg"; + case "conjtype": + filename = "ConjunctionType.svg"; + case "definite": + filename = "Definite.svg"; + case "degree": + filename = "Degree.svg"; + case "mood": + filename = "Mood.svg"; + case "number": + filename = "Number.svg"; + case "pos": + filename = "PartOfSpeech.svg"; + case "person": + filename = "Person.svg"; + case "polarity": + filename = "Polarity.svg"; + case "prontype": + filename = "PronounType.svg"; + case "verbform": + "VerbForm.svg"; + case "voice": + filename = "Voice.svg"; + } + + if (filename == null) { + ErrorHandler.logError( + e: "Missing morphFeature in getMorphSvgLink", + data: {"morphFeature": morphFeature}, + ); + debugPrint("Missing morphFeature in getMorphSvgLink: $morphFeature"); + return null; + } + + return "$baseURL/$filename"; + } + + final key = "${morphFeature.toLowerCase()}${morphTag.toLowerCase()}"; + String? filename; + switch (key) { + case "advtypeadverbial": + filename = "AdverbType_Adverbial.svg"; + case "advtypetim": + filename = "AdverbType_TemporalAdverb.svg"; + case "aspecthab": + filename = "Aspect_Habitual.svg"; + case "aspectimp": + filename = "Aspect_Imperfective.svg"; + case "aspectperf": + filename = "Aspect_Perfective.svg"; + case "aspectprog": + filename = "Aspect_Progressive.svg"; + case "conjtypecoord": + filename = "ConjunctionType_Coordinating.svg"; + case "conjtypesub": + filename = "ConjunctionType_Subordinating.svg"; + case "definitedef": + filename = "Definite_Definite.svg"; + case "definiteind": + filename = "Definite_Indefinite.svg"; + case "degreecmp": + filename = "Degree_Comparative.svg"; + case "degreepos": + filename = "Degree_Positive.svg"; + case "degreesup": + filename = "Degree_Superlative.svg"; + case "moodcnd": + filename = "Mood_Conditional.svg"; + case "moodimp": + filename = "Mood_Imperative.svg"; + case "moodind": + filename = "Mood_Indicative.svg"; + case "moodopt": + filename = "Mood_Optative.svg"; + case "moodsub": + filename = "Mood_Subjunctive.svg"; + case "numberplur": + filename = "Number_Plural.svg"; + case "numbersing": + filename = "Number_Singular.svg"; + case "posadv": + filename = "PartOfSpeech_Adverb.svg"; + case "posadj": + filename = "PartOfSpeech_Adjective.svg"; + case "posadp": + filename = "PartOfSpeech_Adposition.svg"; + case "posaux": + filename = "PartOfSpeech_Auxiliary.svg"; + case "posconj": + filename = "PartOfSpeech_Conjunction.svg"; + case "posdet": + filename = "PartOfSpeech_Determiner.svg"; + case "posnoun": + filename = "PartOfSpeech_Noun.svg"; + case "posnum": + filename = "PartOfSpeech_Numeral.svg"; + case "pospron": + filename = "PartOfSpeech_Pronoun.svg"; + case "pospunct": + filename = "PartOfSpeech_Punctuation.svg"; + case "possconj": + filename = "PartOfSpeech_Subconjunction.svg"; + case "posverb": + filename = "PartOfSpeech_Verb.svg"; + case "person1": + filename = "Person_FirstPerson.svg"; + case "person2": + filename = "Person_SecondPerson.svg"; + case "person3": + filename = "Person_ThirdPerson.svg"; + case "polarityneg": + filename = "Polarity_Negative.svg"; + case "polaritypos": + filename = "Polarity_Positive.svg"; + case "prontypedem": + filename = "PronounType_Demonstrative.svg"; + case "prontypeind": + filename = "PronounType_Indefinite.svg"; + case "prontypeint": + filename = "PronounType_Interrogative.svg"; + case "prontypeneg": + filename = "PronounType_Negative.svg"; + case "prontypeprs": + filename = "PronounType_Personal.svg"; + case "prontyperel": + filename = "PronounType_Relative.svg"; + case "prontypetot": + filename = "PronounType_Total.svg"; + case "tensefut": + filename = "Tense_future.svg"; + case "tenseimp": + filename = "Tense_imperfect.svg"; + case "tensepast": + filename = "Tense_past.svg"; + case "tensepres": + filename = "Tense_present.svg"; + case "verbformfin": + filename = "VerbForm_Finite.svg"; + case "verbformger": + filename = "VerbForm_Gerund.svg"; + case "verbforminf": + filename = "VerbForm_Infinitive.svg"; + case "verbformpart": + filename = "VerbForm_Participle.svg"; + case "voiceact": + filename = "Voice_Active.svg"; + case "voicemid": + filename = "Voice_Middle.svg"; + case "voicepass": + filename = "Voice_Passive.svg"; + } + + if (filename == null) { + ErrorHandler.logError( + e: "Missing morphFeature and morphTag in getMorphSvgLink", + data: {"morphFeature": morphFeature, "morphTag": morphTag}, + ); + debugPrint( + "Missing morphFeature and morphTag in getMorphSvgLink: $morphFeature, $morphTag", + ); + return null; + } + + return "$baseURL/$filename"; +} diff --git a/lib/pangea/common/widgets/customized_svg.dart b/lib/pangea/common/widgets/customized_svg.dart new file mode 100644 index 000000000..6621d9771 --- /dev/null +++ b/lib/pangea/common/widgets/customized_svg.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; + +class CustomizedSvg extends StatelessWidget { + final String svgUrl; + final String cacheKey; + final Map colorReplacements; + final IconData? errorIcon; + + const CustomizedSvg({ + super.key, + required this.svgUrl, + required this.cacheKey, + required this.colorReplacements, + this.errorIcon = Icons.error_outline, + }); + + static final GetStorage _svgStorage = GetStorage('svg_cache'); + + Future _fetchSvg() async { + final cachedSvg = _svgStorage.read(cacheKey); + if (cachedSvg != null) { + return cachedSvg; + } + + final response = await http.get(Uri.parse(svgUrl)); + if (response.statusCode != 200) { + throw Exception('Failed to load SVG: ${response.statusCode}'); + } + + final String svgContent = response.body; + await _svgStorage.write(cacheKey, svgContent); + + return svgContent; + } + + Future _getModifiedSvg() async { + final svgContent = await _fetchSvg(); + String modifiedSvg = svgContent; + modifiedSvg = modifiedSvg.replaceAll("fill=\"none\"", ''); + for (final entry in colorReplacements.entries) { + modifiedSvg = modifiedSvg.replaceAll(entry.key, entry.value); + } + return modifiedSvg; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getModifiedSvg(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Icon(errorIcon); + } else if (snapshot.hasData) { + return SvgPicture.string(snapshot.data!); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart index e9d98f7e5..6e0f2b9c8 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart @@ -1,20 +1,27 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/analytics/enums/morph_categories_enum.dart'; +import 'package:fluffychat/pangea/analytics/utils/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart'; +import 'package:fluffychat/utils/color_value.dart'; class MorphologicalListItem extends StatelessWidget { final Function(String) onPressed; - final String morphCategory; + final String morphFeature; + final String morphTag; final IconData icon; + final String? svgLink; final bool isUnlocked; final bool isSelected; const MorphologicalListItem({ required this.onPressed, - required this.morphCategory, + required this.morphFeature, + required this.morphTag, required this.icon, + this.svgLink, this.isUnlocked = true, this.isSelected = false, super.key, @@ -22,15 +29,37 @@ class MorphologicalListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return WordZoomActivityButton( - icon: Icon(icon), - isSelected: isSelected, - onPressed: () => onPressed(morphCategory), - tooltip: getMorphologicalCategoryCopy( - morphCategory, - context, + return SizedBox( + width: 40, + height: 40, + child: WordZoomActivityButton( + icon: svgLink != null + ? CustomizedSvg( + svgUrl: svgLink!, + cacheKey: svgLink!, + colorReplacements: { + "white": Theme.of(context).cardColor.hexValue.toString(), + "black": Theme.of(context).brightness == Brightness.dark + ? "white" + : "black", + }, + errorIcon: icon, + ) + : Icon(icon), + isSelected: isSelected, + onPressed: () => onPressed(morphFeature), + tooltip: isUnlocked + ? getGrammarCopy( + category: morphFeature, + lemma: morphTag, + context: context, + ) + : getMorphologicalCategoryCopy( + morphFeature, + context, + ), + opacity: (isSelected || !isUnlocked) ? 1 : 0.5, ), - opacity: (isSelected || !isUnlocked) ? 1 : 0.5, ); } } diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_widget.dart index 490abe459..54f443f3c 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/analytics/constants/morph_categories_and_labels.dart'; +import 'package:fluffychat/pangea/analytics/utils/get_svg_link.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphs/morphological_list_item.dart'; @@ -73,8 +74,14 @@ class MorphologicalListWidget extends StatelessWidget { padding: const EdgeInsets.all(2.0), child: MorphologicalListItem( onPressed: setMorphFeature, - morphCategory: morph.morphFeature, + morphFeature: morph.morphFeature, + morphTag: morph.morphTag, icon: getIconForMorphFeature(morph.morphFeature), + svgLink: getMorphSvgLink( + morphFeature: morph.morphFeature, + morphTag: morph.revealed ? morph.morphTag : null, + context: context, + ), isUnlocked: morph.revealed, isSelected: selectedMorphFeature == morph.morphFeature, ),