diff --git a/assets/images/and_book.png b/assets/images/and_book.png new file mode 100644 index 0000000..76c9037 --- /dev/null +++ b/assets/images/and_book.png diff --git a/assets/images/light_ground.png b/assets/images/light_ground.png new file mode 100644 index 0000000..7352426 --- /dev/null +++ b/assets/images/light_ground.png diff --git a/assets/images/next.png b/assets/images/next.png new file mode 100644 index 0000000..25e6aa9 --- /dev/null +++ b/assets/images/next.png diff --git a/assets/images/previous.png b/assets/images/previous.png new file mode 100644 index 0000000..3e9d621 --- /dev/null +++ b/assets/images/previous.png diff --git a/assets/images/title_ground.png b/assets/images/title_ground.png new file mode 100644 index 0000000..c908b35 --- /dev/null +++ b/assets/images/title_ground.png diff --git a/assets/images/video_background.png b/assets/images/video_background.png new file mode 100644 index 0000000..fddb119 --- /dev/null +++ b/assets/images/video_background.png diff --git a/assets/images/video_pause.png b/assets/images/video_pause.png new file mode 100644 index 0000000..8650ddc --- /dev/null +++ b/assets/images/video_pause.png diff --git a/assets/images/video_record.png b/assets/images/video_record.png new file mode 100644 index 0000000..ebf00f4 --- /dev/null +++ b/assets/images/video_record.png diff --git a/assets/images/voice_record_play.png b/assets/images/voice_record_play.png new file mode 100644 index 0000000..d4d9387 --- /dev/null +++ b/assets/images/voice_record_play.png diff --git a/ios/Runner/XSMessageMehtodChannel.swift b/ios/Runner/XSMessageMehtodChannel.swift index 430e093..1d73df6 100644 --- a/ios/Runner/XSMessageMehtodChannel.swift +++ b/ios/Runner/XSMessageMehtodChannel.swift @@ -97,11 +97,8 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { 评测完成后的结果 */ func oralEvaluatingDidEnd(withResult result: [AnyHashable : Any]?, requestId request_id: String?) { - print("评测完成结果") - let resultDict:Dictionary = result?["result"] as! Dictionary - resultData!["result"] = "1" - //分数 - resultData!["overall"] = resultDict["overall"] + var resultDict:Dictionary = result as! Dictionary + resultData! = resultDict; self.evaluateResult() } @@ -109,7 +106,6 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { 评测失败回调 */ func oralEvaluatingDidEndError(_ error: Error?, requestId request_id: String?) { - print("评测失败") messageChannel!.invokeMethod("voiceFail", arguments: error?.localizedDescription) } @@ -117,19 +113,13 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { VAD(前置时间)超时回调 */ func oralEvaluatingDidVADFrontTimeOut() { - print("前置超时--->取消") SSOralEvaluatingManager.share().cancelEvaluate() - if(resultData?.keys.count == 0) { - resultData!["result"] = "0" - self.evaluateResult(); - } } /** VAD(后置时间)超时回调 */ func oralEvaluatingDidVADBackTimeOut() { - print("后置超时--->结束") ///结束回调 SSOralEvaluatingManager.share().stopEvaluate(); } diff --git a/lib/common/request/dao/listen_dao.dart b/lib/common/request/dao/listen_dao.dart index df3d510..8e9353a 100644 --- a/lib/common/request/dao/listen_dao.dart +++ b/lib/common/request/dao/listen_dao.dart @@ -3,6 +3,8 @@ import 'package:wow_english/models/course_process_entity.dart'; import 'package:wow_english/models/follow_read_entity.dart'; import 'package:wow_english/models/listen_entity.dart'; +import '../../../models/read_content_entity.dart'; + class ListenDao { /// 磨耳朵 static Future?> listen() async { @@ -21,4 +23,16 @@ class ListenDao { var data = await requestClient.get(Apis.process,queryParameters: {'courseLessonId':courseLessonId}); return data; } + + ///获取视频跟读内容 + static Future?> readContent(videoFollowReadId) async { + var data = await requestClient.get>(Apis.readContent,queryParameters: {'videoFollowReadId':videoFollowReadId}); + return data; + } + + ///视频跟读提交结果 + static Future followResult(frequency,videoFollowReadId) async { + var data = await requestClient.post(Apis.followResult,data: {'frequency':frequency,'videoFollowReadId':videoFollowReadId}); + return data; + } } diff --git a/lib/common/widgets/textfield_customer_widget.dart b/lib/common/widgets/textfield_customer_widget.dart index a435971..6a78c02 100644 --- a/lib/common/widgets/textfield_customer_widget.dart +++ b/lib/common/widgets/textfield_customer_widget.dart @@ -48,9 +48,9 @@ class _TextFieldCustomerWidgetState extends State { alignment: Alignment.center, decoration: BoxDecoration( image: DecorationImage( - image: AssetImage('${widget.bgImageName}'.assetPng), - fit: BoxFit.fill, - )), + image: AssetImage('${widget.bgImageName}'.assetPng), + fit: BoxFit.fill, + )), child: TextField( inputFormatters: widget.inputFormatters, controller: widget.controller, diff --git a/lib/common/widgets/we_app_bar.dart b/lib/common/widgets/we_app_bar.dart index 55f1608..f28b685 100644 --- a/lib/common/widgets/we_app_bar.dart +++ b/lib/common/widgets/we_app_bar.dart @@ -35,9 +35,7 @@ class WEAppBar extends StatelessWidget implements PreferredSizeWidget { ), leading: leading ?? GestureDetector( - onTap: () { - Navigator.pop(context); - }, + onTap: () => onBack??Navigator.pop(context), child: Container( alignment: Alignment.center, child: Image.asset( diff --git a/lib/generated/json/base/json_convert_content.dart b/lib/generated/json/base/json_convert_content.dart index 6860aa9..6821f57 100644 --- a/lib/generated/json/base/json_convert_content.dart +++ b/lib/generated/json/base/json_convert_content.dart @@ -9,6 +9,7 @@ import 'package:wow_english/models/course_module_entity.dart'; import 'package:wow_english/models/course_process_entity.dart'; import 'package:wow_english/models/follow_read_entity.dart'; import 'package:wow_english/models/listen_entity.dart'; +import 'package:wow_english/models/read_content_entity.dart'; import 'package:wow_english/models/user_entity.dart'; JsonConvert jsonConvert = JsonConvert(); @@ -27,6 +28,7 @@ class JsonConvert { (CourseProcessVideos).toString(): CourseProcessVideos.fromJson, (FollowReadEntity).toString(): FollowReadEntity.fromJson, (ListenEntity).toString(): ListenEntity.fromJson, + (ReadContentEntity).toString(): ReadContentEntity.fromJson, (UserEntity).toString(): UserEntity.fromJson, }; @@ -136,6 +138,9 @@ List? convertListNotNull(dynamic value, {EnumConvertFunction? enumConvert} if([] is M){ return data.map((Map e) => ListenEntity.fromJson(e)).toList() as M; } + if([] is M){ + return data.map((Map e) => ReadContentEntity.fromJson(e)).toList() as M; + } if([] is M){ return data.map((Map e) => UserEntity.fromJson(e)).toList() as M; } diff --git a/lib/generated/json/course_process_entity.g.dart b/lib/generated/json/course_process_entity.g.dart index d91cd08..3c6e33d 100644 --- a/lib/generated/json/course_process_entity.g.dart +++ b/lib/generated/json/course_process_entity.g.dart @@ -78,6 +78,14 @@ CourseProcessReadings $CourseProcessReadingsFromJson(Map json) if (word != null) { courseProcessReadings.word = word; } + final String? recordUrl = jsonConvert.convert(json['recordUrl']); + if (recordUrl != null) { + courseProcessReadings.recordUrl = recordUrl; + } + final String? recordScore = jsonConvert.convert(json['recordScore']); + if (recordScore != null) { + courseProcessReadings.recordScore = recordScore; + } return courseProcessReadings; } @@ -93,6 +101,8 @@ Map $CourseProcessReadingsToJson(CourseProcessReadings entity) data['picUrl'] = entity.picUrl; data['sortOrder'] = entity.sortOrder; data['word'] = entity.word; + data['recordUrl'] = entity.recordUrl; + data['recordScore'] = entity.recordScore; return data; } diff --git a/lib/generated/json/read_content_entity.g.dart b/lib/generated/json/read_content_entity.g.dart new file mode 100644 index 0000000..908470a --- /dev/null +++ b/lib/generated/json/read_content_entity.g.dart @@ -0,0 +1,57 @@ +import 'package:wow_english/generated/json/base/json_convert_content.dart'; +import 'package:wow_english/models/read_content_entity.dart'; + +ReadContentEntity $ReadContentEntityFromJson(Map json) { + final ReadContentEntity readContentEntity = ReadContentEntity(); + final String? createTime = jsonConvert.convert(json['createTime']); + if (createTime != null) { + readContentEntity.createTime = createTime; + } + final String? deleted = jsonConvert.convert(json['deleted']); + if (deleted != null) { + readContentEntity.deleted = deleted; + } + final String? id = jsonConvert.convert(json['id']); + if (id != null) { + readContentEntity.id = id; + } + final String? modifyTime = jsonConvert.convert(json['modifyTime']); + if (modifyTime != null) { + readContentEntity.modifyTime = modifyTime; + } + final int? sortOrder = jsonConvert.convert(json['sortOrder']); + if (sortOrder != null) { + readContentEntity.sortOrder = sortOrder; + } + final int? status = jsonConvert.convert(json['status']); + if (status != null) { + readContentEntity.status = status; + } + final int? videoFollowReadId = jsonConvert.convert(json['videoFollowReadId']); + if (videoFollowReadId != null) { + readContentEntity.videoFollowReadId = videoFollowReadId; + } + final String? videoUrl = jsonConvert.convert(json['videoUrl']); + if (videoUrl != null) { + readContentEntity.videoUrl = videoUrl; + } + final String? word = jsonConvert.convert(json['word']); + if (word != null) { + readContentEntity.word = word; + } + return readContentEntity; +} + +Map $ReadContentEntityToJson(ReadContentEntity entity) { + final Map data = {}; + data['createTime'] = entity.createTime; + data['deleted'] = entity.deleted; + data['id'] = entity.id; + data['modifyTime'] = entity.modifyTime; + data['sortOrder'] = entity.sortOrder; + data['status'] = entity.status; + data['videoFollowReadId'] = entity.videoFollowReadId; + data['videoUrl'] = entity.videoUrl; + data['word'] = entity.word; + return data; +} \ No newline at end of file diff --git a/lib/models/read_content_entity.dart b/lib/models/read_content_entity.dart new file mode 100644 index 0000000..d4046fe --- /dev/null +++ b/lib/models/read_content_entity.dart @@ -0,0 +1,27 @@ +import 'package:wow_english/generated/json/base/json_field.dart'; +import 'package:wow_english/generated/json/read_content_entity.g.dart'; +import 'dart:convert'; + +@JsonSerializable() +class ReadContentEntity { + String? createTime; + String? deleted; + String? id; + String? modifyTime; + int? sortOrder; + int? status; + int? videoFollowReadId; + String? videoUrl; + String? word; + + ReadContentEntity(); + + factory ReadContentEntity.fromJson(Map json) => $ReadContentEntityFromJson(json); + + Map toJson() => $ReadContentEntityToJson(this); + + @override + String toString() { + return jsonEncode(this); + } +} \ No newline at end of file diff --git a/lib/pages/login/setpwd/set_pwd_page.dart b/lib/pages/login/setpwd/set_pwd_page.dart index 3279652..0ed15e7 100644 --- a/lib/pages/login/setpwd/set_pwd_page.dart +++ b/lib/pages/login/setpwd/set_pwd_page.dart @@ -86,43 +86,43 @@ class _SetPassWordPageView extends StatelessWidget { } Widget _buildSetPwdView() => BlocBuilder(builder: (context, state) { - return Scaffold( - body: Container( - color: Colors.white, - child: SafeArea( - child: ListView( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 40.w), - child: Column( + return Scaffold( + body: Container( + color: Colors.white, + child: SafeArea( + child: ListView( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 40.w), + child: Column( + children: [ + 34.verticalSpace, + Row( children: [ - 34.verticalSpace, - Row( - children: [ - Image.asset( - 'wow_logo'.assetPng, - height: 49.w, - width: 83.5.h, - ), - 12.5.horizontalSpace, - Text( - _getTipsText(), - style: TextStyle(fontSize: 16.5.sp, color: const Color(0xFF666666)), - ) - ], + Image.asset( + 'wow_logo'.assetPng, + height: 49.w, + width: 83.5.h, ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, + 12.5.horizontalSpace, + Text( + _getTipsText(), + style: TextStyle(fontSize: 16.5.sp, color: const Color(0xFF666666)), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + 43.verticalSpace, + Row( children: [ - 43.verticalSpace, - Row( - children: [ - Expanded( - child: TextFieldCustomerWidget( + Expanded( + child: TextFieldCustomerWidget( height: 55.h, hitText: '请输入八位以上密码', bgImageName: 'Input_layer_up', @@ -131,27 +131,27 @@ class _SetPassWordPageView extends StatelessWidget { textInputType: TextInputType.emailAddress, onChangeValue: (String value) => bloc.add(PwdEnsureEvent()), )), - 10.horizontalSpace, - Opacity( - opacity: bloc.showPwdIcon ? 1 : 0, - child: Image.asset( - bloc.passwordEnsure ? 'login_pass'.assetPng : 'login_error'.assetPng, - height: 30, - width: 30, - ), - ) - ], - ), - 9.verticalSpace, - Offstage( - offstage: !bloc.passwordLarger, - child: const Text('您已达到密码最大输入数,请妥善调整密码'), - ), - 9.verticalSpace, - Row( - children: [ - Expanded( - child: TextFieldCustomerWidget( + 10.horizontalSpace, + Opacity( + opacity: bloc.showPwdIcon ? 1 : 0, + child: Image.asset( + bloc.passwordEnsure ? 'login_pass'.assetPng : 'login_error'.assetPng, + height: 30, + width: 30, + ), + ) + ], + ), + 9.verticalSpace, + Offstage( + offstage: !bloc.passwordLarger, + child: const Text('您已达到密码最大输入数,请妥善调整密码'), + ), + 9.verticalSpace, + Row( + children: [ + Expanded( + child: TextFieldCustomerWidget( height: 55.h, hitText: '请再次输入相同密码', bgImageName: 'Input_layer_up', @@ -160,70 +160,70 @@ class _SetPassWordPageView extends StatelessWidget { controller: bloc.passWordSecondController, onChangeValue: (String value) => bloc.add(PwdCheckEvent()), )), - 10.horizontalSpace, - Opacity( - opacity: bloc.showCheckPwdIcon ? 1 : 0, - child: Image.asset( - bloc.passwordCheck ? 'login_pass'.assetPng : 'login_error'.assetPng, - height: 30, - width: 30, - ), - ) - ], - ), - 9.verticalSpace, - Offstage( - offstage: bloc.passwordCheck, - child: Text( - '请确认两次输入的密码是否一致', - style: TextStyle(fontSize: 16.sp, color: const Color(0xFF333333)), + 10.horizontalSpace, + Opacity( + opacity: bloc.showCheckPwdIcon ? 1 : 0, + child: Image.asset( + bloc.passwordCheck ? 'login_pass'.assetPng : 'login_error'.assetPng, + height: 30, + width: 30, ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - GestureDetector( - onTap: () { - if (!bloc.ensure) { - return; - } - bloc.add(SetPasswordEvent()); - }, - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - bloc.ensure ? 'login_enter'.assetPng : 'login_enter_dis'.assetPng), - fit: BoxFit.fill), - ), - padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 14.h), - child: Text( - '确定', - style: TextStyle(color: Colors.white, fontSize: 16.sp), - ), - ), - ), - 50.horizontalSpace - ], ) ], ), - ), - 30.horizontalSpace, - Image.asset( - 'steven'.assetPng, - height: 254.h, - width: 100.w, - ) - ], + 9.verticalSpace, + Offstage( + offstage: bloc.passwordCheck, + child: Text( + '请确认两次输入的密码是否一致', + style: TextStyle(fontSize: 16.sp, color: const Color(0xFF333333)), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () { + if (!bloc.ensure) { + return; + } + bloc.add(SetPasswordEvent()); + }, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + bloc.ensure ? 'login_enter'.assetPng : 'login_enter_dis'.assetPng), + fit: BoxFit.fill), + ), + padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 14.h), + child: Text( + '确定', + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ), + ), + 50.horizontalSpace + ], + ) + ], + ), ), + 30.horizontalSpace, + Image.asset( + 'steven'.assetPng, + height: 254.h, + width: 100.w, + ) ], ), - ) - ], - ), - ), + ], + ), + ) + ], ), - ); - }); + ), + ), + ); + }); } diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index d86a450..4b36b9d 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -198,13 +198,9 @@ class TopicPictureBloc extends Bloc { ///先声评测结果 void _voiceXsResult(XSVoiceResultEvent event,Emitter emitter) async { final Map args = event.message as Map; - final result = args['result'] as String; - if (result == '1') { - final overall = args['overall'].toString(); - showToast('测评成功,分数是$overall',duration: const Duration(seconds: 5)); - } else { - showToast('测评失败',duration: const Duration(seconds: 5)); - } + final result = args['result'] as Map; + final overall = result['overall'].toString(); + showToast('测评成功,分数是$overall',duration: const Duration(seconds: 5)); _isVoicing = false; emitter(XSVoiceTestState()); } diff --git a/lib/pages/repeatafter/repeat_after_page.dart b/lib/pages/repeatafter/repeat_after_page.dart index bd6cd37..b1cf2e2 100644 --- a/lib/pages/repeatafter/repeat_after_page.dart +++ b/lib/pages/repeatafter/repeat_after_page.dart @@ -48,7 +48,7 @@ class _RepeatAfterPageView extends StatelessWidget { FollowReadEntity? entity = bloc.listData[index]; return RepeatAfterItem( tapEvent: () { - pushNamed(AppRouteName.readAfterContent); + pushNamed(AppRouteName.readAfterContent,arguments: {'videoFollowReadId':entity?.id}); }, entity: entity, ); diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart new file mode 100644 index 0000000..2311203 --- /dev/null +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart @@ -0,0 +1,192 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:wow_english/common/request/dao/listen_dao.dart'; + +import '../../../common/request/exception.dart'; +import '../../../models/read_content_entity.dart'; +import '../../../utils/loading.dart'; +import '../../../utils/toast_util.dart'; + + +part 'repeat_after_content_event.dart'; +part 'repeat_after_content_state.dart'; + +enum VoiceRecordState { + ///未知 + voiceRecordUnkonw, + ///开始录音 + voiceRecordStat, + ///正在录音 + voiceRecording, + ///录音结束 + voiceRecordEnd +} + +enum VoicePlayState { + ///未知 + unKnow, + ///播放中 + playing, + ///播放完成 + completed, + ///播放终止 + stop +} + +class RepeatAfterContentBloc extends Bloc { + + final String courseLessonId; + + ///是否正在播放视频 + bool _videoPlaying = true; + ///是否需要录音 + bool _isRecord = false; + + Map? _voiceTestResult; + + VoiceRecordState _voiceRecordState = VoiceRecordState.voiceRecordUnkonw; + + bool get videoPlaying => _videoPlaying; + + bool get isRecord => _isRecord; + + VoiceRecordState get voiceRecordState => _voiceRecordState; + + List? _entityList; + + List? get entityList => _entityList ; + + Map? get voiceTestResult => _voiceTestResult; + + late MethodChannel methodChannel; + + late AudioPlayer audioPlayer; + + RepeatAfterContentBloc(this.courseLessonId) : super(RepeatAfterContentInitial()) { + on(_voiceRecordStateChange); + on(_videoPlayStateChange); + on(_recordeVoicePlay); + on(_voiceXsResult); + on(_initVoiceSdk); + on(_requestData); + on(_voiceXsTest); + on(_voiceXsStop); + on(_voiceRecord); + on((event, emit) { + //音频播放器 + audioPlayer = AudioPlayer(); + audioPlayer.onPlayerStateChanged.listen((event) async { + debugPrint('播放状态变化'); + if (event == PlayerState.completed) { + debugPrint('播放完成'); + + } + if (event == PlayerState.stopped) { + debugPrint('播放结束'); + + } + + if (event == PlayerState.playing) { + debugPrint('正在播放中'); + + } + if(isClosed) { + return; + } + + }); + + methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); + methodChannel.setMethodCallHandler((call) async { + if (call.method == 'voiceResult') {//评测结果 + add(XSVoiceResultEvent(call.arguments)); + return; + } + + if (call.method == 'voiceStart') {//评测开始 + debugPrint('评测开始'); + return; + } + + if (call.method == 'voiceEnd') {//评测结束 + debugPrint('评测结束'); + return; + } + + if (call.method == 'voiceFail') {//评测失败 + EasyLoading.showToast('评测失败'); + return; + } + }); + }); + } + + ///请求数据 + void _requestData(RequestDataEvent event,Emitter emitter) async { + try { + await loading(() async { + _entityList = await ListenDao.readContent(courseLessonId); + emitter(RequestDataState()); + }); + } catch (e) { + if (e is ApiException) { + showToast(e.message??'请求失败,请检查网络连接'); + } + } + } + + void _videoPlayStateChange(VideoPlayChangeEvent event,Emitter emitter) async { + _videoPlaying = !_videoPlaying; + emitter(VideoPlayChangeState()); + } + + void _voiceRecord(VoiceRecordEvent event,Emitter emitter) async { + _isRecord = !_isRecord; + emitter(VoiceRecordChangeState()); + } + + void _voiceRecordStateChange(VoiceRecordStateChangeEvent event,Emitter emitter) async { + _voiceRecordState = event.voiceRecordState; + emitter(VoiceRecordStateChange()); + } + + + _initVoiceSdk(XSVoiceInitEvent event,Emitter emitter) async { + methodChannel.invokeMethod('initVoiceSdk',event.data); + } + + ///先声测试 + void _voiceXsTest(XSVoiceTestEvent event,Emitter emitter) async { + await audioPlayer.stop(); + methodChannel.invokeMethod( + 'startVoice', + {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} + ); + emitter(XSVoiceTestState()); + } + + ///终止评测 + void _voiceXsStop(XSVoiceStopEvent event,Emitter emitter) async { + methodChannel.invokeMethod('stopVoice'); + } + + ///先声评测结果 + void _voiceXsResult(XSVoiceResultEvent event,Emitter emitter) async { + final Map args = event.message as Map; + final result = args['result'] as Map; + final overall = result['overall'].toString()??''; + final audioUrl = args['audioUrl'].toString()??''; + _voiceTestResult = {'overall':overall,'audioUrl':audioUrl}; + emitter(XSVoiceTestState()); + } + + ///播放声音 + void _recordeVoicePlay(RecordeVoicePlayEvent event,Emitter emitter) async { + await audioPlayer.stop(); + assert(event.audioUrl.isNotEmpty); + await audioPlayer.play(UrlSource(event.audioUrl)); + } +} diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart new file mode 100644 index 0000000..0c8c408 --- /dev/null +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart @@ -0,0 +1,47 @@ +part of 'repeat_after_content_bloc.dart'; + +@immutable +abstract class RepeatAfterContentEvent {} + +class InitBlocEvent extends RepeatAfterContentEvent {} + +class VideoPlayChangeEvent extends RepeatAfterContentEvent {} + +class VoiceRecordEvent extends RepeatAfterContentEvent {} + +class RequestDataEvent extends RepeatAfterContentEvent {} + +class VoiceRecordStateChangeEvent extends RepeatAfterContentEvent { + final VoiceRecordState voiceRecordState; + VoiceRecordStateChangeEvent(this.voiceRecordState); +} + +///初始化先声SDK +class XSVoiceInitEvent extends RepeatAfterContentEvent { + final Map data; + XSVoiceInitEvent(this.data); +} + +///开始评测 +class XSVoiceTestEvent extends RepeatAfterContentEvent { + final String testWord; + final String type; + final String userId; + XSVoiceTestEvent(this.testWord,this.type,this.userId); +} + +///终止评测 +class XSVoiceStopEvent extends RepeatAfterContentEvent {} + +///评测结果 +class XSVoiceResultEvent extends RepeatAfterContentEvent { + final dynamic message; + XSVoiceResultEvent(this.message); +} + +class RecordeVoicePlayEvent extends RepeatAfterContentEvent { + final String audioUrl; + RecordeVoicePlayEvent(this.audioUrl); +} + + diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_state.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_state.dart new file mode 100644 index 0000000..20ac8ca --- /dev/null +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_state.dart @@ -0,0 +1,16 @@ +part of 'repeat_after_content_bloc.dart'; + +@immutable +abstract class RepeatAfterContentState {} + +class RepeatAfterContentInitial extends RepeatAfterContentState {} + +class VideoPlayChangeState extends RepeatAfterContentState {} + +class VoiceRecordChangeState extends RepeatAfterContentState {} + +class VoiceRecordStateChange extends RepeatAfterContentState {} + +class RequestDataState extends RepeatAfterContentState {} + +class XSVoiceTestState extends RepeatAfterContentState {} diff --git a/lib/pages/repeataftercontent/repeat_after_content_page.dart b/lib/pages/repeataftercontent/repeat_after_content_page.dart index fc6617b..ae98f0b 100644 --- a/lib/pages/repeataftercontent/repeat_after_content_page.dart +++ b/lib/pages/repeataftercontent/repeat_after_content_page.dart @@ -1,18 +1,57 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:wow_english/common/extension/string_extension.dart'; +import 'package:wow_english/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart'; import 'package:wow_english/route/route.dart'; +import '../../common/core/user_util.dart'; +import 'widgets/repeat_video_widget.dart'; + class RepeatAfterContentPage extends StatelessWidget { - const RepeatAfterContentPage({super.key}); + const RepeatAfterContentPage({super.key, this.videoFollowReadId}); + + final String? videoFollowReadId; @override Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RepeatAfterContentBloc(videoFollowReadId ??'') + ..add(InitBlocEvent()) + ..add(RequestDataEvent()) + ..add(XSVoiceInitEvent( + { + 'appKey':'a418', + 'secretKey':'1a16f31f2611bf32fb7b3fc38f5b2c81', + 'userId':UserUtil.getUser()!.id.toString() + } + )), + child: _RepeatAfterContentPage(), + ); + } +} + +class _RepeatAfterContentPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context,state){ + debugPrint('123'); + }, + child: _repeatAfterContentView(), + ); + } + + + Widget _repeatAfterContentView() => BlocBuilder(builder: (context,state){ + final bloc = BlocProvider.of(context); + final String videoUrl = bloc.entityList?.first?.videoUrl??''; return Container( color: Colors.white, child: SafeArea( child: Stack( children: [ + ///返回 Positioned( top: 13.h, child: GestureDetector( @@ -23,10 +62,262 @@ class RepeatAfterContentPage extends StatelessWidget { width: 40.w, ), ), - ) + ), + ///左侧视频区 + Positioned( + top: 53.h, + left: 20.w, + child: Container( + width: 285.w, + height: 299.h, + padding: EdgeInsets.symmetric(horizontal: 50.w,vertical: 50.h), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('video_background'.assetPng), + fit: BoxFit.fill + ), + ), + child: videoUrl.isEmpty?Container(): RepeatVideoWidget(videoUrl: bloc.entityList?.first?.videoUrl,), + ), + ), + ///右侧操作区 + Positioned( + top: 53.h, + right: 25.w, + child: Container( + width: 240.w, + height: 299.h, + padding: EdgeInsets.only( + left: 67.w, + bottom: 40.h + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('light_ground'.assetPng), + fit: BoxFit.fill + ), + ), + child: bloc.isRecord?_buildLongPressWidget():_buildPlayVideoWidget(), + ), + ), + Positioned( + top: 72.h, + left: 274.w, + child: Container( + width: 87.w, + height: 240.h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('and_book'.assetPng), + fit: BoxFit.fill + ), + ), + ), + ), + ///跟读 + Positioned( + top: 29.h, + left: 65.w, + child: Container( + width: 185.w, + height: 48.h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('title_ground'.assetPng), + fit: BoxFit.fill + ), + ), + child: Text( + 'read title', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 21.sp + ), + ), + ), + ), ], ), ), ); + }); + + ///播放中 + Widget _buildPlayVideoWidget() { + return BlocBuilder( + builder: (context,state){ + final bloc = BlocProvider.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + SizedBox( + height: 23.h, + width: 33.w, + ), + // IconButton( + // onPressed: (){}, + // icon: Image.asset( + // 'previous'.assetPng, + // height: 23.h, + // width: 23.w, + // ) + // ), + IconButton( + onPressed:() => bloc.add(VideoPlayChangeEvent()), + icon: Image.asset( + 'video_pause'.assetPng, + height: 50.h, + width: 50.w, + ) + ), + SizedBox( + height: 23.h, + width: 23.w, + ), + // IconButton( + // onPressed: (){}, + // icon: Image.asset( + // 'next'.assetPng, + // height: 23.h, + // width: 23.w, + // ) + // ) + ], + ), + Row( + children: [ + SizedBox( + height: 23.h, + width: 23.w, + ), + 10.horizontalSpace, + Column( + children: [ + 20.verticalSpace, + IconButton( + onPressed: ()=>bloc.add(VoiceRecordEvent()), + icon: Image.asset( + 'video_record'.assetPng, + height: 53.h, + width: 53.w, + ) + ), + Text( + '录音', + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 14.sp + ), + ) + ], + ), + // Container( + // height: 22.h, + // width: 37.w, + // decoration: BoxDecoration( + // color: const Color(0xFF56CE5F), + // borderRadius: BorderRadius.circular(10.r) + // ), + // child: Text( + // '1.0x', + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp + // ), + // ), + // ) + ], + ) + ], + ); + }); } + + ///长按录音 + Widget _buildLongPressWidget() => BlocBuilder( + builder: (context,state){ + final bloc = BlocProvider.of(context); + final voiceResult = bloc.voiceTestResult; + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: bloc.voiceRecordState != VoiceRecordState.voiceRecordEnd && voiceResult == null, + child: Column( + children: [ + Container( + height: 45.h, + width: 45.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xFF40E04B), + borderRadius: BorderRadius.circular(22.5.r) + ), + child: Text( + voiceResult?['overall'].toString()??'0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 17.sp + ), + ), + ), + IconButton( + onPressed: (){ + if(voiceResult != null) { + bloc.add(RecordeVoicePlayEvent(voiceResult['audioUrl']??'')); + } + }, + icon: Image.asset( + 'voice_record_play'.assetPng, + height: 30.h, + width: 30.w, + ) + ), + Text( + '录音', + textAlign: TextAlign.center, + style: TextStyle( + color: const Color(0xFF666666), + fontSize: 11.sp + ), + ), + ], + ), + ), + GestureDetector( + onTap: () => bloc.add(VoiceRecordEvent()), + onLongPress: () { + bloc.add(XSVoiceTestEvent(bloc.entityList?.first?.word??'', '0', UserUtil.getUser()!.id.toString())); + }, + onLongPressStart: (LongPressStartDetails details) { + bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordStat)); + }, + onLongPressEnd: (LongPressEndDetails details) { + bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordEnd)); + }, + onLongPressUp: () { + + }, + child: Image.asset( + 'video_record'.assetPng, + height: 78.h, + width: 78.w, + ), + ), + Text( + '按住录音', + textAlign: TextAlign.center, + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 16.sp + ), + ), + ], + ); + }); } \ No newline at end of file diff --git a/lib/pages/repeataftercontent/widgets/repeat_video_widget.dart b/lib/pages/repeataftercontent/widgets/repeat_video_widget.dart new file mode 100644 index 0000000..70434a6 --- /dev/null +++ b/lib/pages/repeataftercontent/widgets/repeat_video_widget.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:video_player/video_player.dart'; + +import '../bloc/repeat_after_content_bloc.dart'; + +class RepeatVideoWidget extends StatefulWidget { + const RepeatVideoWidget({super.key, this.videoUrl}); + final String? videoUrl; + + @override + State createState() { + return _RepeatVideoWidgetState(); + } +} + +class _RepeatVideoWidgetState extends State { + VideoPlayerController? _controller; + String _currentTime = '00:00'; + String _totalTime = '00:00'; + double _playDegree = 0.5; + + String formatDuration(Duration duration) { + String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return "$minutes:$seconds"; + } + + void _addListener() { + _controller!.addListener(() { + if(_controller!.value.isInitialized) { + if (_controller!.value.isPlaying) { + setState(() { + double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble(); + int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60); + _currentTime = formatDuration(_controller!.value.position); + _playDegree = currentSecond/totalSecond; + if(_playDegree < 0) { + _playDegree = 0.0; + } + + if(_playDegree > 1) { + _playDegree = 1.0; + } + }); + } + } + }); + } + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.videoUrl??'') + ..initialize().then((_){ + setState(() { + _currentTime = formatDuration(_controller!.value.position); + _totalTime = formatDuration(_controller!.value.duration); + _controller!.setLooping(true); + _controller!.setVolume(100); + _controller!.play(); + }); + _addListener(); + }); + } + + @override + Widget build(BuildContext context) { + return Center( + child: _controller!.value.isInitialized ? BlocListener( + listener: (context, state){ + if (state is VideoPlayChangeState) { + if (context.read().videoPlaying) { + _controller!.play(); + } else { + _controller!.pause(); + } + } + }, + child: SizedBox( + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + width: 1.0, + color: const Color(0xFF140C10), + ), + borderRadius: BorderRadius.circular(5.r) + ), + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, + child: VideoPlayer(_controller!), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Slider( + min:0, + max: 1.0, + activeColor: const Color(0xFF78B72D), + inactiveColor: const Color(0xFF7E756C), + onChangeStart: (value) { + + }, + onChangeEnd: (value) { + + }, + onChanged: (value) { + + }, value: _playDegree, + )) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_currentTime), + Text(_totalTime), + ], + ) + ], + ), + ) + ) + : + Container( + color: Colors.white, + child: Text( + '视频加载中....', + style: TextStyle( + fontSize: 20.sp, + color: Colors.black + ), + ), + ), + ); + } + + @override + void dispose() { + _controller?.dispose(); + _controller?.removeListener(() {}); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/pages/video/lookvideo/widgets/video_widget.dart b/lib/pages/video/lookvideo/widgets/video_widget.dart index 82dde51..b76750c 100644 --- a/lib/pages/video/lookvideo/widgets/video_widget.dart +++ b/lib/pages/video/lookvideo/widgets/video_widget.dart @@ -1,6 +1,6 @@ import 'package:common_utils/common_utils.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:video_player/video_player.dart'; import 'package:wow_english/common/extension/string_extension.dart'; @@ -169,14 +169,21 @@ class _VideoWidgetState extends State { }, icon: Image.asset( 'video_stop'.assetPng, - width: 70, - height: 70, + width: 70.w, + height: 70.h, ), ), ) ], ): Container( color: Colors.white, + child: Text( + '视频加载中....', + style: TextStyle( + fontSize: 20.sp, + color: Colors.black + ), + ), ), ), ); diff --git a/lib/route/route.dart b/lib/route/route.dart index 15a170b..9769771 100644 --- a/lib/route/route.dart +++ b/lib/route/route.dart @@ -79,8 +79,8 @@ class AppRouter { } return CupertinoPageRoute( builder: (_) => HomePage( - moduleId: moduleId, - )); + moduleId: moduleId, + )); case AppRouteName.fogPwd: return CupertinoPageRoute(builder: (_) => const ForgetPasswordHomePage()); case AppRouteName.lesson: @@ -99,7 +99,7 @@ class AppRouter { return CupertinoPageRoute(builder: (_) => const UserPage()); case AppRouteName.userInformation: return CupertinoPageRoute(builder: (_) => const UserInformationPage()); - /*case AppRouteName.userModifyInformation: + /*case AppRouteName.userModifyInformation: return CupertinoPageRoute(builder: (_) { ModifyUserInformationType argument = ModifyUserInformationType.name; if (settings.arguments != null) { @@ -118,16 +118,11 @@ class AppRouter { final title = (settings.arguments as Map)['title'] as String?; return CupertinoPageRoute( builder: (_) => LookVideoPage( - videoUrl: videoUrl, - typeTitle: title, - )); - /*case AppRouteName.setPwd: - var map = settings.arguments as Map; - final phoneNum = map['phoneNumber'] as String?; - final smsCode = map['smsCode'] as String?; - final pageType = map['pageType'] as SetPwdPageType; - return CupertinoPageRoute( - builder: (_) => SetPassWordPage( + videoUrl: videoUrl, + typeTitle: title, + )); + /*case AppRouteName.setPwd: + case AppRouteName.setPwd: phoneNum: phoneNum, smsCode: smsCode, pageType: pageType, @@ -137,12 +132,16 @@ class AppRouter { final webViewTitle = (settings.arguments as Map)['webViewTitle'] as String; return CupertinoPageRoute( builder: (_) => WowWebViewPage( - urlStr: urlStr, - webViewTitle: webViewTitle, - )); + urlStr: urlStr, + webViewTitle: webViewTitle, + )); case AppRouteName.readAfterContent: + var videoFollowReadId = ''; + if (settings.arguments != null) { + videoFollowReadId = (settings.arguments as Map)['videoFollowReadId'] as String??''; + } return CupertinoPageRoute( - builder: (_) => const RepeatAfterContentPage()); + builder: (_) => RepeatAfterContentPage(videoFollowReadId:videoFollowReadId)); case AppRouteName.tab: return PageRouteBuilder( opaque: false,