1719 grammar detailed view in analytics (#1728)
* feat: grammar analytics details page --------- Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>pull/1605/head
							parent
							
								
									8988cce68a
								
							
						
					
					
						commit
						a71f519700
					
				@ -0,0 +1,100 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/config/app_config.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_details_popup/lemma_usage_dots.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_details_popup/lemma_use_example_messages.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
 | 
			
		||||
 | 
			
		||||
class AnalyticsDetailsViewContent extends StatelessWidget {
 | 
			
		||||
  final Widget title;
 | 
			
		||||
  final Widget subtitle;
 | 
			
		||||
  final Widget headerContent;
 | 
			
		||||
  final Widget xpIcon;
 | 
			
		||||
  final ConstructIdentifier constructId;
 | 
			
		||||
 | 
			
		||||
  const AnalyticsDetailsViewContent({
 | 
			
		||||
    required this.title,
 | 
			
		||||
    required this.subtitle,
 | 
			
		||||
    required this.xpIcon,
 | 
			
		||||
    required this.headerContent,
 | 
			
		||||
    required this.constructId,
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  ConstructUses get construct => constructId.constructUses;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final Color textColor = Theme.of(context).brightness != Brightness.light
 | 
			
		||||
        ? construct.lemmaCategory.color
 | 
			
		||||
        : construct.lemmaCategory.darkColor;
 | 
			
		||||
 | 
			
		||||
    return SingleChildScrollView(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          title,
 | 
			
		||||
          const SizedBox(height: 16.0),
 | 
			
		||||
          subtitle,
 | 
			
		||||
          const SizedBox(height: 16.0),
 | 
			
		||||
          headerContent,
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.symmetric(vertical: 16.0),
 | 
			
		||||
            child: Image.network(
 | 
			
		||||
              "${AppConfig.assetsBaseURL}/${AnalyticsConstants.popupDividerFileName}",
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          Row(
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              xpIcon,
 | 
			
		||||
              const SizedBox(width: 16.0),
 | 
			
		||||
              Text(
 | 
			
		||||
                "${construct.points} XP",
 | 
			
		||||
                style: Theme.of(context).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                      color: textColor,
 | 
			
		||||
                    ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          const SizedBox(height: 20),
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.all(20.0),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                LemmaUseExampleMessages(construct: construct),
 | 
			
		||||
                // Writing exercise section
 | 
			
		||||
                LemmaUsageDots(
 | 
			
		||||
                  construct: construct,
 | 
			
		||||
                  category: LearningSkillsEnum.writing,
 | 
			
		||||
                  tooltip: L10n.of(context).writingExercisesTooltip,
 | 
			
		||||
                  icon: Symbols.edit_square,
 | 
			
		||||
                ),
 | 
			
		||||
                // Listening exercise section
 | 
			
		||||
                LemmaUsageDots(
 | 
			
		||||
                  construct: construct,
 | 
			
		||||
                  category: LearningSkillsEnum.hearing,
 | 
			
		||||
                  tooltip: L10n.of(context).listeningExercisesTooltip,
 | 
			
		||||
                  icon: Symbols.hearing,
 | 
			
		||||
                ),
 | 
			
		||||
                // Reading exercise section
 | 
			
		||||
                LemmaUsageDots(
 | 
			
		||||
                  construct: construct,
 | 
			
		||||
                  category: LearningSkillsEnum.reading,
 | 
			
		||||
                  tooltip: L10n.of(context).readingExercisesTooltip,
 | 
			
		||||
                  icon: Symbols.two_pager,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,174 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup_content.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
 | 
			
		||||
 | 
			
		||||
class MorphDetailsView extends StatelessWidget {
 | 
			
		||||
  final ConstructIdentifier constructId;
 | 
			
		||||
 | 
			
		||||
  const MorphDetailsView({
 | 
			
		||||
    required this.constructId,
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  ConstructUses get _construct => constructId.constructUses;
 | 
			
		||||
  String get _morphFeature => constructId.category;
 | 
			
		||||
  String get _morphTag => constructId.lemma;
 | 
			
		||||
 | 
			
		||||
  String _categoryCopy(
 | 
			
		||||
    BuildContext context,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (_morphFeature.toLowerCase() == "other") {
 | 
			
		||||
      return L10n.of(context).other;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ConstructTypeEnum.morph.getDisplayCopy(
 | 
			
		||||
          _morphFeature,
 | 
			
		||||
          context,
 | 
			
		||||
        ) ??
 | 
			
		||||
        _morphFeature;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> _getDefinition(BuildContext context) => MorphInfoRepo.get(
 | 
			
		||||
        feature: _construct.category,
 | 
			
		||||
        tag: _construct.lemma,
 | 
			
		||||
      ).then((value) => value ?? L10n.of(context).meaningNotFound);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final Color textColor = Theme.of(context).brightness != Brightness.light
 | 
			
		||||
        ? _construct.lemmaCategory.color
 | 
			
		||||
        : _construct.lemmaCategory.darkColor;
 | 
			
		||||
 | 
			
		||||
    return AnalyticsDetailsViewContent(
 | 
			
		||||
      title: Row(
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            width: 32.0,
 | 
			
		||||
            height: 32.0,
 | 
			
		||||
            child: MorphIcon(
 | 
			
		||||
              morphFeature: _morphFeature,
 | 
			
		||||
              morphTag: _morphTag,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          const SizedBox(width: 10.0),
 | 
			
		||||
          Text(
 | 
			
		||||
            getGrammarCopy(
 | 
			
		||||
                  category: _morphFeature,
 | 
			
		||||
                  lemma: _morphTag,
 | 
			
		||||
                  context: context,
 | 
			
		||||
                ) ??
 | 
			
		||||
                _morphTag,
 | 
			
		||||
            style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      subtitle: Row(
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            width: 24.0,
 | 
			
		||||
            height: 24.0,
 | 
			
		||||
            child: MorphIcon(morphFeature: _morphFeature, morphTag: null),
 | 
			
		||||
          ),
 | 
			
		||||
          const SizedBox(width: 10.0),
 | 
			
		||||
          Text(
 | 
			
		||||
            _categoryCopy(context),
 | 
			
		||||
            style: Theme.of(context).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                  color: textColor,
 | 
			
		||||
                ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      headerContent: Padding(
 | 
			
		||||
        padding: const EdgeInsets.all(25.0),
 | 
			
		||||
        child: Align(
 | 
			
		||||
          alignment: Alignment.topLeft,
 | 
			
		||||
          child: FutureBuilder(
 | 
			
		||||
            future: _getDefinition(context),
 | 
			
		||||
            builder: (
 | 
			
		||||
              BuildContext context,
 | 
			
		||||
              AsyncSnapshot<String?> snapshot,
 | 
			
		||||
            ) {
 | 
			
		||||
              if (snapshot.hasData) {
 | 
			
		||||
                return RichText(
 | 
			
		||||
                  text: TextSpan(
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                      color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                    ),
 | 
			
		||||
                    children: <TextSpan>[
 | 
			
		||||
                      TextSpan(
 | 
			
		||||
                        text: L10n.of(context).meaningSectionHeader,
 | 
			
		||||
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
			
		||||
                              fontWeight: FontWeight.bold,
 | 
			
		||||
                            ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      TextSpan(
 | 
			
		||||
                        text: "  ${snapshot.data!}",
 | 
			
		||||
                        style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              } else if (snapshot.hasError) {
 | 
			
		||||
                return Wrap(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(
 | 
			
		||||
                      L10n.of(context).meaningSectionHeader,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
			
		||||
                            fontWeight: FontWeight.bold,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(
 | 
			
		||||
                      width: 10,
 | 
			
		||||
                    ),
 | 
			
		||||
                    Text(
 | 
			
		||||
                      L10n.of(context).meaningNotFound,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              } else {
 | 
			
		||||
                return Wrap(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(
 | 
			
		||||
                      L10n.of(context).meaningSectionHeader,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
			
		||||
                            fontWeight: FontWeight.bold,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const SizedBox(
 | 
			
		||||
                      width: 10,
 | 
			
		||||
                    ),
 | 
			
		||||
                    const TextLoadingShimmer(width: 100),
 | 
			
		||||
                  ],
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      xpIcon: CircleAvatar(
 | 
			
		||||
        radius: 16.0,
 | 
			
		||||
        backgroundColor: _construct.lemmaCategory.color,
 | 
			
		||||
        child: const Icon(
 | 
			
		||||
          Icons.star,
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
          size: 20.0,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      constructId: constructId,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,99 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get_storage/get_storage.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:fluffychat/pangea/common/config/environment.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/common/network/requests.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/common/network/urls.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
 | 
			
		||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_response.dart';
 | 
			
		||||
import 'package:fluffychat/widgets/matrix.dart';
 | 
			
		||||
 | 
			
		||||
class _APICallCacheItem {
 | 
			
		||||
  final DateTime time;
 | 
			
		||||
  final Future<MorphInfoResponse> future;
 | 
			
		||||
 | 
			
		||||
  _APICallCacheItem(this.time, this.future);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MorphInfoRepo {
 | 
			
		||||
  static final GetStorage _morphMeaningStorage =
 | 
			
		||||
      GetStorage('morph_meaning_storage');
 | 
			
		||||
  static final shortTermCache = <String, _APICallCacheItem>{};
 | 
			
		||||
  static const int _cacheDurationMinutes = 1;
 | 
			
		||||
 | 
			
		||||
  static void set(MorphInfoRequest request, MorphInfoResponse response) {
 | 
			
		||||
    _morphMeaningStorage.write(request.storageKey, response.toJson());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<MorphInfoResponse> _fetch(MorphInfoRequest request) async {
 | 
			
		||||
    try {
 | 
			
		||||
      final Requests req = Requests(
 | 
			
		||||
        choreoApiKey: Environment.choreoApiKey,
 | 
			
		||||
        accessToken: MatrixState.pangeaController.userController.accessToken,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      final Response res = await req.post(
 | 
			
		||||
        url: PApiUrls.morphDictionary,
 | 
			
		||||
        body: request.toJson(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
 | 
			
		||||
      final response = MorphInfoResponse.fromJson(decodedBody);
 | 
			
		||||
 | 
			
		||||
      set(request, response);
 | 
			
		||||
 | 
			
		||||
      return response;
 | 
			
		||||
    } catch (e, s) {
 | 
			
		||||
      debugPrint('Error fetching morph info: $e');
 | 
			
		||||
      return Future.error(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<MorphInfoResponse> _get(MorphInfoRequest request) async {
 | 
			
		||||
    request.userL1 == request.userL1.split('-').first;
 | 
			
		||||
    request.userL2 == request.userL2.split('-').first;
 | 
			
		||||
 | 
			
		||||
    final cachedJson = _morphMeaningStorage.read(request.storageKey);
 | 
			
		||||
    if (cachedJson != null) {
 | 
			
		||||
      return MorphInfoResponse.fromJson(cachedJson);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final _APICallCacheItem? cachedCall = shortTermCache[request.storageKey];
 | 
			
		||||
    if (cachedCall != null) {
 | 
			
		||||
      if (DateTime.now().difference(cachedCall.time).inMinutes <
 | 
			
		||||
          _cacheDurationMinutes) {
 | 
			
		||||
        return cachedCall.future;
 | 
			
		||||
      } else {
 | 
			
		||||
        shortTermCache.remove(request.storageKey);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final future = _fetch(request);
 | 
			
		||||
    shortTermCache[request.storageKey] =
 | 
			
		||||
        _APICallCacheItem(DateTime.now(), future);
 | 
			
		||||
    return future;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future<String?> get({
 | 
			
		||||
    required String feature,
 | 
			
		||||
    required String tag,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final res = await _get(
 | 
			
		||||
      MorphInfoRequest(
 | 
			
		||||
        userL1:
 | 
			
		||||
            MatrixState.pangeaController.languageController.userL1?.langCode ??
 | 
			
		||||
                LanguageKeys.defaultLanguage,
 | 
			
		||||
        userL2:
 | 
			
		||||
            MatrixState.pangeaController.languageController.userL2?.langCode ??
 | 
			
		||||
                LanguageKeys.defaultLanguage,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return res.getFeatureByCode(feature)?.getTagByCode(tag)?.l1Description;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
class MorphInfoRequest {
 | 
			
		||||
  final String userL1;
 | 
			
		||||
  final String userL2;
 | 
			
		||||
 | 
			
		||||
  MorphInfoRequest({
 | 
			
		||||
    required this.userL1,
 | 
			
		||||
    required this.userL2,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return {
 | 
			
		||||
      'user_l1': userL1,
 | 
			
		||||
      'user_l2': userL2,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) =>
 | 
			
		||||
      identical(this, other) ||
 | 
			
		||||
      other is MorphInfoRequest &&
 | 
			
		||||
          userL1 == other.userL1 &&
 | 
			
		||||
          userL2 == other.userL2;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => userL1.hashCode ^ userL2.hashCode;
 | 
			
		||||
 | 
			
		||||
  String get storageKey {
 | 
			
		||||
    return userL1 + userL2;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,112 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
 | 
			
		||||
class MorphologicalTag {
 | 
			
		||||
  final String code;
 | 
			
		||||
  final String l2Title;
 | 
			
		||||
  final String l1Title;
 | 
			
		||||
  final String l1Description;
 | 
			
		||||
 | 
			
		||||
  MorphologicalTag({
 | 
			
		||||
    required this.code,
 | 
			
		||||
    required this.l2Title,
 | 
			
		||||
    required this.l1Title,
 | 
			
		||||
    required this.l1Description,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  factory MorphologicalTag.fromJson(Map<String, dynamic> json) {
 | 
			
		||||
    return MorphologicalTag(
 | 
			
		||||
      code: json['code'],
 | 
			
		||||
      l2Title: json['l2_title'],
 | 
			
		||||
      l1Title: json['l1_title'],
 | 
			
		||||
      l1Description: json['l1_description'],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return {
 | 
			
		||||
      'code': code,
 | 
			
		||||
      'l2_title': l2Title,
 | 
			
		||||
      'l1_title': l1Title,
 | 
			
		||||
      'l1_description': l1Description,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MorphologicalFeature {
 | 
			
		||||
  final String code;
 | 
			
		||||
  final String l2Title;
 | 
			
		||||
  final String l1Title;
 | 
			
		||||
  final List<MorphologicalTag> tags;
 | 
			
		||||
 | 
			
		||||
  MorphologicalFeature({
 | 
			
		||||
    required this.code,
 | 
			
		||||
    required this.l2Title,
 | 
			
		||||
    required this.l1Title,
 | 
			
		||||
    required this.tags,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  factory MorphologicalFeature.fromJson(Map<String, dynamic> json) {
 | 
			
		||||
    final tagsFromJson = json['tags'] as List;
 | 
			
		||||
    final List<MorphologicalTag> tagsList =
 | 
			
		||||
        tagsFromJson.map((tag) => MorphologicalTag.fromJson(tag)).toList();
 | 
			
		||||
 | 
			
		||||
    return MorphologicalFeature(
 | 
			
		||||
      code: json['code'],
 | 
			
		||||
      l2Title: json['l2_title'],
 | 
			
		||||
      l1Title: json['l1_title'],
 | 
			
		||||
      tags: tagsList,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return {
 | 
			
		||||
      'code': code,
 | 
			
		||||
      'l2_title': l2Title,
 | 
			
		||||
      'l1_title': l1Title,
 | 
			
		||||
      'tags': tags.map((tag) => tag.toJson()).toList(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  MorphologicalTag? getTagByCode(String code) {
 | 
			
		||||
    return tags.firstWhereOrNull(
 | 
			
		||||
        (tag) => tag.code.toLowerCase() == code.toLowerCase());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MorphInfoResponse {
 | 
			
		||||
  final String userL1;
 | 
			
		||||
  final String userL2;
 | 
			
		||||
  final List<MorphologicalFeature> features;
 | 
			
		||||
 | 
			
		||||
  MorphInfoResponse({
 | 
			
		||||
    required this.userL1,
 | 
			
		||||
    required this.userL2,
 | 
			
		||||
    required this.features,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  factory MorphInfoResponse.fromJson(Map<String, dynamic> json) {
 | 
			
		||||
    final featuresFromJson = json['features'] as List;
 | 
			
		||||
    final List<MorphologicalFeature> featuresList = featuresFromJson
 | 
			
		||||
        .map((feature) => MorphologicalFeature.fromJson(feature))
 | 
			
		||||
        .toList();
 | 
			
		||||
 | 
			
		||||
    return MorphInfoResponse(
 | 
			
		||||
      userL1: json['user_l1'],
 | 
			
		||||
      userL2: json['user_l2'],
 | 
			
		||||
      features: featuresList,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return {
 | 
			
		||||
      'user_l1': userL1,
 | 
			
		||||
      'user_l2': userL2,
 | 
			
		||||
      'features': features.map((feature) => feature.toJson()).toList(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  MorphologicalFeature? getFeatureByCode(String code) {
 | 
			
		||||
    return features.firstWhereOrNull(
 | 
			
		||||
        (feature) => feature.code.toLowerCase() == code.toLowerCase());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue