diff --git a/assets/images/section_finish_again.png b/assets/images/section_finish_again.png new file mode 100644 index 0000000..59753cb --- /dev/null +++ b/assets/images/section_finish_again.png diff --git a/assets/images/section_finish_next.png b/assets/images/section_finish_next.png new file mode 100644 index 0000000..6b5af89 --- /dev/null +++ b/assets/images/section_finish_next.png diff --git a/assets/images/section_finish_steve.png b/assets/images/section_finish_steve.png new file mode 100644 index 0000000..e47384d --- /dev/null +++ b/assets/images/section_finish_steve.png diff --git a/lib/common/request/apis.dart b/lib/common/request/apis.dart index 70e4f71..5682d84 100644 --- a/lib/common/request/apis.dart +++ b/lib/common/request/apis.dart @@ -89,9 +89,12 @@ class Apis { /// 进入课堂 static const String enterClass = 'course/enter/class'; - /// 退出课堂 + /// 退出课堂(非完整、中断) static const String exitClass = 'course/exit/class'; + /// 结束课堂(完整) + static const String endClass = 'course/end/class'; + /// 商品列表 static const String productList = 'order/course/combo/list'; diff --git a/lib/common/request/dao/listen_dao.dart b/lib/common/request/dao/listen_dao.dart index 318f1d5..1808fea 100644 --- a/lib/common/request/dao/listen_dao.dart +++ b/lib/common/request/dao/listen_dao.dart @@ -4,6 +4,7 @@ import 'package:wow_english/models/follow_read_entity.dart'; import 'package:wow_english/models/listen_entity.dart'; import '../../../models/read_content_entity.dart'; +import '../../../utils/date_util.dart'; class ListenDao { /// 磨耳朵 @@ -43,8 +44,14 @@ class ListenDao { } ///退出课堂 - static Future exitClass(courseLessonId,currentStep,currentTime) async { - var data = await requestClient.post(Apis.exitClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime}); + static Future exitClass(courseLessonId,currentStep,{int? currentTime}) async { + var data = await requestClient.post(Apis.exitClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime ?? getTimestampOfSecond()}); + return data; + } + + ///完成课堂 + static Future endClass(courseLessonId,currentStep,{int? currentTime}) async { + var data = await requestClient.post(Apis.endClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime ?? getTimestampOfSecond()}); return data; } } diff --git a/lib/pages/section/bloc/section_bloc.dart b/lib/pages/section/bloc/section_bloc.dart index 9943fd2..01d47a4 100644 --- a/lib/pages/section/bloc/section_bloc.dart +++ b/lib/pages/section/bloc/section_bloc.dart @@ -49,6 +49,7 @@ class SectionBloc extends Bloc { SectionBloc(this._courseUnitEntity, this._courseUnitDetail, this._pageController, this._listController) : super(LessonInitial()) { on(_requestData); on(_requestExitClass); + on(_requestEndClass); on(_requestEnterClass); on(_requestVideoLesson); on(_pageControllerChange); @@ -95,7 +96,16 @@ class SectionBloc extends Bloc { } void _requestExitClass(RequestExitClassEvent event,Emitter emitter) async { - await ListenDao.exitClass(event.courseLessonId,event.currentStep,event.currentTime); + await ListenDao.exitClass(event.courseLessonId,event.currentStep); + } + + void _requestEndClass(RequestEndClassEvent event,Emitter emitter) async { + final obj = await ListenDao.endClass(event.courseLessonId,event.currentStep); + if (event.autoNextSection) { + final nextCourseSection = getNextCourseSectionBySort(int.parse(event.courseLessonId)); + ///进入课堂 + add(RequestEnterClassEvent(nextCourseSection!.id.toString() ?? '', nextCourseSection.courseType)); + } } void _pageControllerChange(CurrentUnitIndexChangeEvent event, @@ -108,4 +118,29 @@ class SectionBloc extends Bloc { int unlockPageCount() { return _courseUnitEntity.courseUnitVOList?.indexWhereOrNull((element) => element.lock == true) ?? 1; } + + CourseSectionEntity? getNextCourseSectionBySort(int courseLessonId) { + final curCourseSectionEntity = _courseSectionDatas?.firstWhere((element) => element.id == courseLessonId); + final curSort = curCourseSectionEntity?.sortOrder ?? 0; + CourseSectionEntity? nextCourseSectionEntity = _courseSectionDatas?.firstWhere((element) => element.sortOrder == curSort + 1); + if (nextCourseSectionEntity != null) { + return nextCourseSectionEntity; + } else { + ///跨unit选lesson + final curCourseUnitDetail = _courseUnitEntity.courseUnitVOList?.firstWhere((element) => element.id == curCourseSectionEntity?.courseUnitId); + if (curCourseUnitDetail != null) { + final nextCourseUnitDetail = _courseUnitEntity.courseUnitVOList?.firstWhere((element) => element.sortOrder == 0); + if (nextCourseUnitDetail != null) { + ///pageView翻页了,可能需要预加载 todo + return null; + } else { + ///最后一个unit了 + return null; + } + } else { + ///找不到对应的unitDetail,理论上不可能 + return null; + } + } + } } diff --git a/lib/pages/section/bloc/section_event.dart b/lib/pages/section/bloc/section_event.dart index c21f727..0738d28 100644 --- a/lib/pages/section/bloc/section_event.dart +++ b/lib/pages/section/bloc/section_event.dart @@ -27,6 +27,16 @@ class RequestExitClassEvent extends SectionEvent { RequestExitClassEvent(this.courseLessonId,this.currentStep,this.currentTime); } +///结束课堂 +class RequestEndClassEvent extends SectionEvent { + final String courseLessonId; + final String currentStep; + final String currentTime; + ///自动进入下一环节 + final bool autoNextSection; + RequestEndClassEvent(this.courseLessonId,this.currentStep,this.currentTime,{this.autoNextSection = false}); +} + ///页面切换 class CurrentUnitIndexChangeEvent extends SectionEvent { final int unitIndex; diff --git a/lib/pages/section/section_page.dart b/lib/pages/section/section_page.dart index d5b9a2e..c267178 100644 --- a/lib/pages/section/section_page.dart +++ b/lib/pages/section/section_page.dart @@ -6,6 +6,7 @@ import 'package:nested_scroll_views/material.dart'; import 'package:wow_english/common/core/user_util.dart'; import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/models/course_unit_entity.dart'; +import 'package:wow_english/pages/section/section_type.dart'; import 'package:wow_english/pages/section/widgets/home_video_item.dart'; import 'package:wow_english/pages/section/widgets/section_bouns_item.dart'; import 'package:wow_english/pages/section/widgets/section_header_widget.dart'; @@ -52,15 +53,15 @@ class _SectionPageView extends StatelessWidget { if (state is RequestVideoLessonState) { final videoUrl = bloc.processEntity?.videos?.videoUrl ?? ''; var title = ''; - if (state.type == 1) { + if (state.type == SectionType.song.value) { title = 'song'; } - if (state.type == 2) { + if (state.type == SectionType.video.value) { title = 'video'; } - if (state.type == 5) { + if (state.type == SectionType.bouns.value) { title = 'bonus'; } @@ -70,22 +71,21 @@ class _SectionPageView extends StatelessWidget { pushNamed(AppRouteName.lookVideo, arguments: { 'videoUrl': videoUrl, 'title': title, - 'courseLessonId': state.courseLessonId + 'courseLessonId': state.courseLessonId, + 'isTopic': true }).then((value) { if (value != null) { - Map dataMap = value as Map; - bloc.add(RequestExitClassEvent( - dataMap['courseLessonId']!, - '0', - dataMap['currentTime']!, - )); + Map dataMap = value as Map; + bloc.add(RequestEndClassEvent(dataMap['courseLessonId']!, '0', + dataMap['currentTime']!, autoNextSection: dataMap['nextSection'] as bool)); } }); return; } if (state is RequestEnterClassState) { - if (state.courseType != 3 && state.courseType != 4) { + if (state.courseType != SectionType.practice.value + && state.courseType != SectionType.pictureBook.value) { ///视频类型 ///获取视频课程内容 bloc.add(RequestVideoLessonEvent( @@ -93,7 +93,7 @@ class _SectionPageView extends StatelessWidget { return; } - if (state.courseType == 4) { + if (state.courseType == SectionType.pictureBook.value) { //绘本 pushNamed(AppRouteName.reading, arguments: {'courseLessonId': state.courseLessonId}) @@ -107,7 +107,7 @@ class _SectionPageView extends StatelessWidget { return; } - if (state.courseType == 3) { + if (state.courseType == SectionType.practice.value) { //练习 pushNamed(AppRouteName.topicPic, arguments: {'courseLessonId': state.courseLessonId}) @@ -214,7 +214,7 @@ Widget _itemTransCard(int index, BuildContext context) { scrollDirection: Axis.horizontal, itemBuilder: (BuildContext context, int index) { CourseSectionEntity sectionData = bloc.courseSectionDatas![index]; - if (sectionData.courseType == 5) { + if (sectionData.courseType == SectionType.bouns.value) { //彩蛋 return GestureDetector( onTap: () { diff --git a/lib/pages/section/section_type.dart b/lib/pages/section/section_type.dart new file mode 100644 index 0000000..48c375e --- /dev/null +++ b/lib/pages/section/section_type.dart @@ -0,0 +1,36 @@ +///环节类型 +enum SectionType { + ///儿歌 + song, + + ///视频 + video, + + ///练习 + practice, + + ///绘本 + pictureBook, + + ///彩蛋 + bouns +} + +extension SectionTypeExtension on SectionType { + int get value { + switch (this) { + case SectionType.song: + return 1; + case SectionType.video: + return 2; + case SectionType.practice: + return 3; + case SectionType.pictureBook: + return 4; + case SectionType.bouns: + return 5; + default: + throw ArgumentError('Unknown section type'); + } + } +} diff --git a/lib/pages/section/subsection/base_section/bloc.dart b/lib/pages/section/subsection/base_section/bloc.dart new file mode 100644 index 0000000..f62c8ea --- /dev/null +++ b/lib/pages/section/subsection/base_section/bloc.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wow_english/common/extension/string_extension.dart'; + +import '../../../../route/route.dart'; +import 'event.dart'; +import 'state.dart'; + +abstract class BaseSectionBloc extends Bloc { + BaseSectionBloc(super.initialState); + + bool isCompleteDialogShow = false; + + // 这里可以定义一些通用的逻辑 + void completeSection(final VoidCallback? nextSectionTap) { + // 逻辑来标记步骤为已完成 + // 比如更新状态 + if (isCompleteDialogShow) { + return; + } + isCompleteDialogShow = true; + showDialog( + context: AppRouter.context, + barrierDismissible: false, + barrierColor: Colors.black54, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.transparent, + content: SizedBox( + width: double.infinity, // 宽度设置为无限,使其尽可能铺满屏幕 + height: MediaQuery.of(context).size.height * 0.6, // 高度设置为屏幕高度的60% + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, // 图片之间分配空间 + children: [ + // 左侧可点击的图片 + Expanded( + flex: 1, + child: GestureDetector( + onTap: () { + isCompleteDialogShow = false; + popPage(); + add(SectionAgainEvent() as E); + }, + child: Image.asset('section_finish_again'.assetPng), + ), + ), + // 中间的图片 + Expanded( + flex: 2, + child: Image.asset('section_finish_steve'.assetPng), + ), + // 右侧可点击的图片 + Expanded( + flex: 1, + child: GestureDetector( + onTap: () { + // 处理右侧图片的点击事件 + isCompleteDialogShow = false; + popPage(); + nextSectionTap!(); + }, + child: Image.asset('section_finish_next'.assetPng), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/section/subsection/base_section/event.dart b/lib/pages/section/subsection/base_section/event.dart new file mode 100644 index 0000000..d864bb2 --- /dev/null +++ b/lib/pages/section/subsection/base_section/event.dart @@ -0,0 +1,10 @@ +abstract class BaseSectionEvent {} + +///环节完成(结束) +class SectionCompleted extends BaseSectionEvent {} + +///环节再来一次 +class SectionAgainEvent extends BaseSectionEvent {} + +///下一个环节 +class SectionNextEvent extends BaseSectionEvent {} \ No newline at end of file diff --git a/lib/pages/section/subsection/base_section/state.dart b/lib/pages/section/subsection/base_section/state.dart new file mode 100644 index 0000000..17a32cf --- /dev/null +++ b/lib/pages/section/subsection/base_section/state.dart @@ -0,0 +1,3 @@ +abstract class BaseSectionState {} + +class SectionCompleted extends BaseSectionState {} diff --git a/lib/pages/video/lookvideo/bloc/look_video_bloc.dart b/lib/pages/video/lookvideo/bloc/look_video_bloc.dart index 9c10502..f4ecd61 100644 --- a/lib/pages/video/lookvideo/bloc/look_video_bloc.dart +++ b/lib/pages/video/lookvideo/bloc/look_video_bloc.dart @@ -1,15 +1,27 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:video_player/video_player.dart'; +import 'package:wow_english/pages/section/subsection/base_section/bloc.dart'; +import 'package:wow_english/pages/section/subsection/base_section/event.dart'; +import 'package:wow_english/pages/section/subsection/base_section/state.dart'; part 'look_video_event.dart'; part 'look_video_state.dart'; -class LookVideoBloc extends Bloc { +class LookVideoBloc extends BaseSectionBloc { VideoPlayerController? _controller; - LookVideoBloc() : super(LookVideoInitial()) { + final String? _videoUrl; + String? get videoUrl => _videoUrl; + final String? _typeTitle; + String? get typeTitle => _typeTitle; + final String? _courseLessonId; + String? get courseLessonId => _courseLessonId; + final bool _isTopic; + bool get isTopic => _isTopic; + + LookVideoBloc(this._videoUrl, this._typeTitle, this._courseLessonId, this._isTopic) : super(LookVideoInitial()) { on((event, emit) { // TODO: implement event handler }); diff --git a/lib/pages/video/lookvideo/bloc/look_video_event.dart b/lib/pages/video/lookvideo/bloc/look_video_event.dart index 95bd813..ae5bd1e 100644 --- a/lib/pages/video/lookvideo/bloc/look_video_event.dart +++ b/lib/pages/video/lookvideo/bloc/look_video_event.dart @@ -1,4 +1,4 @@ part of 'look_video_bloc.dart'; @immutable -abstract class LookVideoEvent {} +abstract class LookVideoEvent extends BaseSectionEvent {} diff --git a/lib/pages/video/lookvideo/bloc/look_video_state.dart b/lib/pages/video/lookvideo/bloc/look_video_state.dart index 9d7102f..cf00985 100644 --- a/lib/pages/video/lookvideo/bloc/look_video_state.dart +++ b/lib/pages/video/lookvideo/bloc/look_video_state.dart @@ -1,7 +1,7 @@ part of 'look_video_bloc.dart'; @immutable -abstract class LookVideoState {} +abstract class LookVideoState extends BaseSectionState {} class LookVideoInitial extends LookVideoState {} diff --git a/lib/pages/video/lookvideo/look_video_page.dart b/lib/pages/video/lookvideo/look_video_page.dart index b535aa7..de0a169 100644 --- a/lib/pages/video/lookvideo/look_video_page.dart +++ b/lib/pages/video/lookvideo/look_video_page.dart @@ -1,29 +1,38 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart'; import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart'; -class LookVideoPage extends StatefulWidget { - const LookVideoPage({super.key, this.videoUrl, this.typeTitle, this.courseLessonId}); +class LookVideoPage extends StatelessWidget { + const LookVideoPage( + {super.key, this.videoUrl, this.typeTitle, this.courseLessonId, this.isTopic = false}); final String? videoUrl; final String? typeTitle; final String? courseLessonId; + final bool isTopic; @override - State createState() { - return _LookVideoPageState(); - } -} - -class _LookVideoPageState extends State { - @override Widget build(BuildContext context) { - return Container( - color: Colors.white, - child: VideoWidget( - videoUrl: widget.videoUrl??'', - typeTitle: widget.typeTitle, - courseLessonId: widget.courseLessonId??'', - ), + return BlocProvider( + create: (BuildContext context) => LookVideoBloc(videoUrl, typeTitle, courseLessonId, isTopic), + child: Builder(builder: (context) => _buildPage(context)), ); } -} \ No newline at end of file +} + +Widget _buildPage(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final bloc = BlocProvider.of(context); + return Container( + color: Colors.white, + child: VideoWidget( + videoUrl: bloc.videoUrl ?? '', + typeTitle: bloc.typeTitle ?? '', + courseLessonId: bloc.courseLessonId ?? '', + isTopic: bloc.isTopic, + ) + ); + } + ); +} diff --git a/lib/pages/video/lookvideo/widgets/video_widget.dart b/lib/pages/video/lookvideo/widgets/video_widget.dart index 11d2cd3..f61f6ae 100644 --- a/lib/pages/video/lookvideo/widgets/video_widget.dart +++ b/lib/pages/video/lookvideo/widgets/video_widget.dart @@ -1,18 +1,21 @@ import 'package:common_utils/common_utils.dart'; 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 'package:wow_english/common/extension/string_extension.dart'; +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart'; import 'package:wow_english/route/route.dart'; import 'video_opera_widget.dart'; class VideoWidget extends StatefulWidget { - const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = ''}); + const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = '', this.isTopic = false}); final String videoUrl; final String? typeTitle; final String courseLessonId; + final bool isTopic; @override State createState() { @@ -51,6 +54,12 @@ class _VideoWidgetState extends State { _playDegree = 0.0; } }); + } else if (_controller!.value.isCompleted) { + context.read().completeSection((){ + String currentTime = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toString(); + popPage(data:{'courseLessonId':widget.courseLessonId,'currentTime':currentTime, + 'nextSection':widget.isTopic}); + } as VoidCallback); } } }); @@ -114,7 +123,7 @@ class _VideoWidgetState extends State { setState(() { _currentTime = formatDuration(_controller!.value.position); _totalTime = formatDuration(_controller!.value.duration); - _controller!.setLooping(true); + _controller!.setLooping(!widget.isTopic); _controller!.setVolume(100); _controller!.play(); }); diff --git a/lib/route/route.dart b/lib/route/route.dart index 49020a9..d1c3343 100644 --- a/lib/route/route.dart +++ b/lib/route/route.dart @@ -190,11 +190,14 @@ class AppRouter { final title = (settings.arguments as Map)['title'] as String?; final courseLessonId = (settings.arguments as Map)['courseLessonId'] as String?; + ///是否是课程内的视频环节,用于播放结束判断要不要再来一次以及下一环节用 + final isTopic = (settings.arguments as Map)['isTopic'] as bool? ?? false; return CupertinoPageRoute( builder: (_) => LookVideoPage( videoUrl: videoUrl, typeTitle: title, courseLessonId: courseLessonId, + isTopic: isTopic, )); /*case AppRouteName.setPwd: case AppRouteName.setPwd: diff --git a/lib/utils/date_util.dart b/lib/utils/date_util.dart new file mode 100644 index 0000000..ec4a00d --- /dev/null +++ b/lib/utils/date_util.dart @@ -0,0 +1,14 @@ + +///获取当前时间(单位:秒) +int getTimestampOfSecond() { + // 获取当前时间 + DateTime now = DateTime.now(); + + // 获取自Unix纪元以来的毫秒数 + int milliseconds = now.millisecondsSinceEpoch; + + // 将毫秒数转换为秒 + int seconds = milliseconds ~/ 1000; + + return seconds; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d4cf9f2..c5c7030 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,7 +90,7 @@ dependencies: # 富文本插件 https://pub.dev/packages/extended_text extended_text: ^11.0.1 # 视频播放 https://pub.dev/packages/video_player - video_player: ^2.7.0 + video_player: ^2.8.6 # UI适配 https://pub.dev/packages/responsive_framework responsive_framework: ^1.0.0 # 音频播放 https://pub.dev/packages/audioplayers