diff --git a/lib/generated/json/base/json_convert_content.dart b/lib/generated/json/base/json_convert_content.dart index 4e904a5..b60f603 100644 --- a/lib/generated/json/base/json_convert_content.dart +++ b/lib/generated/json/base/json_convert_content.dart @@ -16,6 +16,7 @@ import 'package:wow_english/models/follow_read_entity.dart'; import 'package:wow_english/models/listen_entity.dart'; import 'package:wow_english/models/product_entity.dart'; import 'package:wow_english/models/read_content_entity.dart'; +import 'package:wow_english/models/singsound_result_detail_entity.dart'; import 'package:wow_english/models/user_entity.dart'; JsonConvert jsonConvert = JsonConvert(); @@ -232,6 +233,10 @@ class JsonConvert { return data.map((Map e) => ReadContentEntity.fromJson(e)).toList() as M; } + if ([] is M) { + return data.map((Map e) => + SingsoundResultDetailEntity.fromJson(e)).toList() as M; + } if ([] is M) { return data.map((Map e) => UserEntity.fromJson(e)).toList() as M; @@ -278,6 +283,8 @@ class JsonConvertClassCollection { (ListenEntity).toString(): ListenEntity.fromJson, (ProductEntity).toString(): ProductEntity.fromJson, (ReadContentEntity).toString(): ReadContentEntity.fromJson, + (SingsoundResultDetailEntity).toString(): SingsoundResultDetailEntity + .fromJson, (UserEntity).toString(): UserEntity.fromJson, }; diff --git a/lib/generated/json/course_process_entity.g.dart b/lib/generated/json/course_process_entity.g.dart index 1f387ec..2fe4246 100644 --- a/lib/generated/json/course_process_entity.g.dart +++ b/lib/generated/json/course_process_entity.g.dart @@ -1,5 +1,7 @@ import 'package:wow_english/generated/json/base/json_convert_content.dart'; import 'package:wow_english/models/course_process_entity.dart'; +import 'package:wow_english/models/singsound_result_detail_entity.dart'; + CourseProcessEntity $CourseProcessEntityFromJson(Map json) { final CourseProcessEntity courseProcessEntity = CourseProcessEntity(); @@ -114,6 +116,15 @@ CourseProcessReadings $CourseProcessReadingsFromJson( if (recordScore != null) { courseProcessReadings.recordScore = recordScore; } + final List< + SingsoundResultDetailEntity>? resultDetails = (json['resultDetails'] as List< + dynamic>?)?.map( + (e) => + jsonConvert.convert( + e) as SingsoundResultDetailEntity).toList(); + if (resultDetails != null) { + courseProcessReadings.resultDetails = resultDetails; + } return courseProcessReadings; } @@ -132,6 +143,7 @@ Map $CourseProcessReadingsToJson( data['word'] = entity.word; data['recordUrl'] = entity.recordUrl; data['recordScore'] = entity.recordScore; + data['resultDetails'] = entity.resultDetails?.map((v) => v.toJson()).toList(); return data; } @@ -149,6 +161,7 @@ extension CourseProcessReadingsExtension on CourseProcessReadings { String? word, String? recordUrl, String? recordScore, + List? resultDetails, }) { return CourseProcessReadings() ..audioUrl = audioUrl ?? this.audioUrl @@ -162,7 +175,8 @@ extension CourseProcessReadingsExtension on CourseProcessReadings { ..sortOrder = sortOrder ?? this.sortOrder ..word = word ?? this.word ..recordUrl = recordUrl ?? this.recordUrl - ..recordScore = recordScore ?? this.recordScore; + ..recordScore = recordScore ?? this.recordScore + ..resultDetails = resultDetails ?? this.resultDetails; } } diff --git a/lib/generated/json/singsound_result_detail_entity.g.dart b/lib/generated/json/singsound_result_detail_entity.g.dart new file mode 100644 index 0000000..b45d179 --- /dev/null +++ b/lib/generated/json/singsound_result_detail_entity.g.dart @@ -0,0 +1,126 @@ +import 'package:wow_english/generated/json/base/json_convert_content.dart'; +import 'package:wow_english/models/singsound_result_detail_entity.dart'; + +SingsoundResultDetailEntity $SingsoundResultDetailEntityFromJson( + Map json) { + final SingsoundResultDetailEntity singsoundResultDetailEntity = SingsoundResultDetailEntity(); + final int? dpType = jsonConvert.convert(json['dp_type']); + if (dpType != null) { + singsoundResultDetailEntity.dpType = dpType; + } + final int? tonescore = jsonConvert.convert(json['tonescore']); + if (tonescore != null) { + singsoundResultDetailEntity.tonescore = tonescore; + } + final int? dur = jsonConvert.convert(json['dur']); + if (dur != null) { + singsoundResultDetailEntity.dur = dur; + } + final int? liaisonref = jsonConvert.convert(json['liaisonref']); + if (liaisonref != null) { + singsoundResultDetailEntity.liaisonref = liaisonref; + } + final int? stressref = jsonConvert.convert(json['stressref']); + if (stressref != null) { + singsoundResultDetailEntity.stressref = stressref; + } + final int? senseref = jsonConvert.convert(json['senseref']); + if (senseref != null) { + singsoundResultDetailEntity.senseref = senseref; + } + final int? start = jsonConvert.convert(json['start']); + if (start != null) { + singsoundResultDetailEntity.start = start; + } + final int? liaisonscore = jsonConvert.convert(json['liaisonscore']); + if (liaisonscore != null) { + singsoundResultDetailEntity.liaisonscore = liaisonscore; + } + final int? fluency = jsonConvert.convert(json['fluency']); + if (fluency != null) { + singsoundResultDetailEntity.fluency = fluency; + } + final String? char = jsonConvert.convert(json['char']); + if (char != null) { + singsoundResultDetailEntity.char = char; + } + final int? toneref = jsonConvert.convert(json['toneref']); + if (toneref != null) { + singsoundResultDetailEntity.toneref = toneref; + } + final int? stressscore = jsonConvert.convert(json['stressscore']); + if (stressscore != null) { + singsoundResultDetailEntity.stressscore = stressscore; + } + final int? score = jsonConvert.convert(json['score']); + if (score != null) { + singsoundResultDetailEntity.score = score; + } + final int? end = jsonConvert.convert(json['end']); + if (end != null) { + singsoundResultDetailEntity.end = end; + } + final int? sensescore = jsonConvert.convert(json['sensescore']); + if (sensescore != null) { + singsoundResultDetailEntity.sensescore = sensescore; + } + return singsoundResultDetailEntity; +} + +Map $SingsoundResultDetailEntityToJson( + SingsoundResultDetailEntity entity) { + final Map data = {}; + data['dp_type'] = entity.dpType; + data['tonescore'] = entity.tonescore; + data['dur'] = entity.dur; + data['liaisonref'] = entity.liaisonref; + data['stressref'] = entity.stressref; + data['senseref'] = entity.senseref; + data['start'] = entity.start; + data['liaisonscore'] = entity.liaisonscore; + data['fluency'] = entity.fluency; + data['char'] = entity.char; + data['toneref'] = entity.toneref; + data['stressscore'] = entity.stressscore; + data['score'] = entity.score; + data['end'] = entity.end; + data['sensescore'] = entity.sensescore; + return data; +} + +extension SingsoundResultDetailEntityExtension on SingsoundResultDetailEntity { + SingsoundResultDetailEntity copyWith({ + int? dpType, + int? tonescore, + int? dur, + int? liaisonref, + int? stressref, + int? senseref, + int? start, + int? liaisonscore, + int? fluency, + String? char, + int? toneref, + int? stressscore, + int? score, + int? end, + int? sensescore, + }) { + return SingsoundResultDetailEntity() + ..dpType = dpType ?? this.dpType + ..tonescore = tonescore ?? this.tonescore + ..dur = dur ?? this.dur + ..liaisonref = liaisonref ?? this.liaisonref + ..stressref = stressref ?? this.stressref + ..senseref = senseref ?? this.senseref + ..start = start ?? this.start + ..liaisonscore = liaisonscore ?? this.liaisonscore + ..fluency = fluency ?? this.fluency + ..char = char ?? this.char + ..toneref = toneref ?? this.toneref + ..stressscore = stressscore ?? this.stressscore + ..score = score ?? this.score + ..end = end ?? this.end + ..sensescore = sensescore ?? this.sensescore; + } +} \ No newline at end of file diff --git a/lib/models/course_process_entity.dart b/lib/models/course_process_entity.dart index 80e1cf1..4d18509 100644 --- a/lib/models/course_process_entity.dart +++ b/lib/models/course_process_entity.dart @@ -2,6 +2,8 @@ import 'package:wow_english/generated/json/base/json_field.dart'; import 'package:wow_english/generated/json/course_process_entity.g.dart'; import 'dart:convert'; +import 'package:wow_english/models/singsound_result_detail_entity.dart'; + @JsonSerializable() class CourseProcessEntity { int? currentStep; @@ -36,6 +38,7 @@ class CourseProcessReadings { String? word; String? recordUrl; String? recordScore; + List? resultDetails; CourseProcessReadings(); diff --git a/lib/models/singsound_result_detail_entity.dart b/lib/models/singsound_result_detail_entity.dart new file mode 100644 index 0000000..f8dec89 --- /dev/null +++ b/lib/models/singsound_result_detail_entity.dart @@ -0,0 +1,52 @@ +import 'package:wow_english/generated/json/base/json_field.dart'; +import 'package:wow_english/generated/json/singsound_result_detail_entity.g.dart'; +import 'dart:convert'; +export 'package:wow_english/generated/json/singsound_result_detail_entity.g.dart'; + +@JsonSerializable() +class SingsoundResultDetailEntity { + @JSONField(name: "dp_type") + late int? dpType; + late int? tonescore; + late int? dur; + late int? liaisonref; + late int? stressref; + late int? senseref; + late int? start; + late int? liaisonscore; + late int? fluency; + late String char; + late int? toneref; + late int? stressscore; + late int score; + late int? end; + late int? sensescore; + + SingsoundResultDetailEntity(); + + // 只接受两个参数的构造函数 + SingsoundResultDetailEntity.withCharAndScore(this.char, this.score) { + dpType = null; + tonescore = null; + dur = null; + liaisonref = null; + stressref = null; + senseref = null; + start = null; + liaisonscore = null; + fluency = null; + toneref = null; + stressscore = null; + end = null; + sensescore = null; + } + + factory SingsoundResultDetailEntity.fromJson(Map json) => $SingsoundResultDetailEntityFromJson(json); + + Map toJson() => $SingsoundResultDetailEntityToJson(this); + + @override + String toString() { + return jsonEncode(this); + } +} \ No newline at end of file diff --git a/lib/pages/reading/bloc/reading_bloc.dart b/lib/pages/reading/bloc/reading_bloc.dart index e3d16bb..82011df 100644 --- a/lib/pages/reading/bloc/reading_bloc.dart +++ b/lib/pages/reading/bloc/reading_bloc.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; @@ -18,6 +19,7 @@ import '../../../common/request/exception.dart'; import '../../../common/utils/click_with_music_controller.dart'; import '../../../common/utils/show_star_reward_dialog.dart'; import '../../../models/course_process_entity.dart'; +import '../../../models/singsound_result_detail_entity.dart'; import '../../../models/voice_result_type.dart'; import '../../../route/route.dart'; import '../../../utils/loading.dart'; @@ -308,6 +310,41 @@ class ReadingPageBloc return currentPageData()?.word?.trim() ?? ''; } + /// 结合单词分数返回带颜色的TextSpan数组 + List? displayContent() { + // return _textController.text.isNotEmpty ? _textController.text : readingContent(); + // 创建用于展示每个单词的 TextSpan 列表 + if (currentPageData() == null) { + return null; + } + List resultDetails; + if (currentPageData()?.resultDetails != null) { + resultDetails = currentPageData()!.resultDetails!; + } else { + List? wordList = currentPageData()?.word?.split(RegExp(r'\s+')); + resultDetails = wordList! + .map( + (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) + .toList(); + } + List textSpans = resultDetails.asMap().entries.map((entry) { + int index = entry.key; + SingsoundResultDetailEntity detail = entry.value; + // Check if this is the last word in the list + bool isLastWord = index == resultDetails.length - 1; + return TextSpan( + text: '${detail.char}${isLastWord ? '' : ' '}', + style: TextStyle( + color: detail.score > 80 + ? const Color(0XFF35C137) + : const Color(0xFF333333), + fontSize: 20.sp, + ), + ); + }).toList(); + return textSpans; + } + void nextPage() { if (currentPage >= dataCount()) { sectionComplete(() { @@ -362,12 +399,23 @@ class ReadingPageBloc final result = args['result'] as Map; Log.d("_voiceXsResult result=$result"); final overall = result['overall'].toString(); + List resultDetailsJsons = result['details']; + // 提取 score 和 char 字段 + List detailEntities = []; + for (var detail in resultDetailsJsons) { + int score = detail['score'] as int; + String char = detail['char'] as String; + detailEntities + .add(SingsoundResultDetailEntity.withCharAndScore(char, score)); + } ///todo 后面可以考虑要不要传自己的服务器 final recordFileUrl = args['audioUrl'].toString(); int score = int.parse(overall); currentPageData()?.recordScore = overall; currentPageData()?.recordUrl = recordFileUrl.assetMp3; + currentPageData()?.resultDetails = detailEntities; + add(OnXSVoiceStateChangeEvent()); final voiceResult = VoiceResultType.fromScore(score); if (voiceResult.lottieFilePath != null) { diff --git a/lib/pages/reading/reading_page.dart b/lib/pages/reading/reading_page.dart index f427558..888f671 100644 --- a/lib/pages/reading/reading_page.dart +++ b/lib/pages/reading/reading_page.dart @@ -154,7 +154,7 @@ class _ReadingPage extends StatelessWidget { Align( alignment: Alignment.bottomLeft, child: Container( - color: const Color(0x80FFFFFF), + color: const Color(0xCCFFFFFF), child: Row( children: [ 5.horizontalSpace, @@ -175,13 +175,15 @@ class _ReadingPage extends StatelessWidget { width: 10.w, ), Expanded( - child: Text( - bloc.readingContent(), - style: TextStyle( - color: const Color(0xFF333333), fontSize: 21.sp), - maxLines: 2, - overflow: TextOverflow.ellipsis, - )), + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: bloc.displayContent(), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), SizedBox( width: 10.w, ),