feat: widget for customizing SVG colors (#1498)
* feat: widget for customizing SVG colors * feat: replace morph icons with customized morph SVGs
This commit is contained in:
parent
fd3f851995
commit
3d85d2ec9f
4 changed files with 298 additions and 11 deletions
184
lib/pangea/analytics/utils/get_svg_link.dart
Normal file
184
lib/pangea/analytics/utils/get_svg_link.dart
Normal file
|
|
@ -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";
|
||||
}
|
||||
67
lib/pangea/common/widgets/customized_svg.dart
Normal file
67
lib/pangea/common/widgets/customized_svg.dart
Normal file
|
|
@ -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<String, String> 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<String> _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<String> _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<String>(
|
||||
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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue