Commit 46675a89f53e32a1b709d8512fbff8971f33ead9

Authored by 吴启风
1 parent e3a0f013

feat:过渡页-视频环节

assets/images/section_finish_again.png 0 → 100644

17.3 KB

assets/images/section_finish_next.png 0 → 100644

13.8 KB

assets/images/section_finish_steve.png 0 → 100644

34.7 KB

lib/common/request/apis.dart
... ... @@ -89,9 +89,12 @@ class Apis {
89 89 /// 进入课堂
90 90 static const String enterClass = 'course/enter/class';
91 91  
92   - /// 退出课堂
  92 + /// 退出课堂(非完整、中断)
93 93 static const String exitClass = 'course/exit/class';
94 94  
  95 + /// 结束课堂(完整)
  96 + static const String endClass = 'course/end/class';
  97 +
95 98 /// 商品列表
96 99 static const String productList = 'order/course/combo/list';
97 100  
... ...
lib/common/request/dao/listen_dao.dart
... ... @@ -4,6 +4,7 @@ import 'package:wow_english/models/follow_read_entity.dart';
4 4 import 'package:wow_english/models/listen_entity.dart';
5 5  
6 6 import '../../../models/read_content_entity.dart';
  7 +import '../../../utils/date_util.dart';
7 8  
8 9 class ListenDao {
9 10 /// 磨耳朵
... ... @@ -43,8 +44,14 @@ class ListenDao {
43 44 }
44 45  
45 46 ///退出课堂
46   - static Future exitClass(courseLessonId,currentStep,currentTime) async {
47   - var data = await requestClient.post(Apis.exitClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime});
  47 + static Future exitClass(courseLessonId,currentStep,{int? currentTime}) async {
  48 + var data = await requestClient.post(Apis.exitClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime ?? getTimestampOfSecond()});
  49 + return data;
  50 + }
  51 +
  52 + ///完成课堂
  53 + static Future endClass(courseLessonId,currentStep,{int? currentTime}) async {
  54 + var data = await requestClient.post(Apis.endClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime ?? getTimestampOfSecond()});
48 55 return data;
49 56 }
50 57 }
... ...
lib/pages/section/bloc/section_bloc.dart
... ... @@ -49,6 +49,7 @@ class SectionBloc extends Bloc<SectionEvent, SectionState> {
49 49 SectionBloc(this._courseUnitEntity, this._courseUnitDetail, this._pageController, this._listController) : super(LessonInitial()) {
50 50 on<RequestDataEvent>(_requestData);
51 51 on<RequestExitClassEvent>(_requestExitClass);
  52 + on<RequestEndClassEvent>(_requestEndClass);
52 53 on<RequestEnterClassEvent>(_requestEnterClass);
53 54 on<RequestVideoLessonEvent>(_requestVideoLesson);
54 55 on<CurrentUnitIndexChangeEvent>(_pageControllerChange);
... ... @@ -95,7 +96,16 @@ class SectionBloc extends Bloc&lt;SectionEvent, SectionState&gt; {
95 96 }
96 97  
97 98 void _requestExitClass(RequestExitClassEvent event,Emitter<SectionState> emitter) async {
98   - await ListenDao.exitClass(event.courseLessonId,event.currentStep,event.currentTime);
  99 + await ListenDao.exitClass(event.courseLessonId,event.currentStep);
  100 + }
  101 +
  102 + void _requestEndClass(RequestEndClassEvent event,Emitter<SectionState> emitter) async {
  103 + final obj = await ListenDao.endClass(event.courseLessonId,event.currentStep);
  104 + if (event.autoNextSection) {
  105 + final nextCourseSection = getNextCourseSectionBySort(int.parse(event.courseLessonId));
  106 + ///进入课堂
  107 + add(RequestEnterClassEvent(nextCourseSection!.id.toString() ?? '', nextCourseSection.courseType));
  108 + }
99 109 }
100 110  
101 111 void _pageControllerChange(CurrentUnitIndexChangeEvent event,
... ... @@ -108,4 +118,29 @@ class SectionBloc extends Bloc&lt;SectionEvent, SectionState&gt; {
108 118 int unlockPageCount() {
109 119 return _courseUnitEntity.courseUnitVOList?.indexWhereOrNull((element) => element.lock == true) ?? 1;
110 120 }
  121 +
  122 + CourseSectionEntity? getNextCourseSectionBySort(int courseLessonId) {
  123 + final curCourseSectionEntity = _courseSectionDatas?.firstWhere((element) => element.id == courseLessonId);
  124 + final curSort = curCourseSectionEntity?.sortOrder ?? 0;
  125 + CourseSectionEntity? nextCourseSectionEntity = _courseSectionDatas?.firstWhere((element) => element.sortOrder == curSort + 1);
  126 + if (nextCourseSectionEntity != null) {
  127 + return nextCourseSectionEntity;
  128 + } else {
  129 + ///跨unit选lesson
  130 + final curCourseUnitDetail = _courseUnitEntity.courseUnitVOList?.firstWhere((element) => element.id == curCourseSectionEntity?.courseUnitId);
  131 + if (curCourseUnitDetail != null) {
  132 + final nextCourseUnitDetail = _courseUnitEntity.courseUnitVOList?.firstWhere((element) => element.sortOrder == 0);
  133 + if (nextCourseUnitDetail != null) {
  134 + ///pageView翻页了,可能需要预加载 todo
  135 + return null;
  136 + } else {
  137 + ///最后一个unit了
  138 + return null;
  139 + }
  140 + } else {
  141 + ///找不到对应的unitDetail,理论上不可能
  142 + return null;
  143 + }
  144 + }
  145 + }
111 146 }
... ...
lib/pages/section/bloc/section_event.dart
... ... @@ -27,6 +27,16 @@ class RequestExitClassEvent extends SectionEvent {
27 27 RequestExitClassEvent(this.courseLessonId,this.currentStep,this.currentTime);
28 28 }
29 29  
  30 +///结束课堂
  31 +class RequestEndClassEvent extends SectionEvent {
  32 + final String courseLessonId;
  33 + final String currentStep;
  34 + final String currentTime;
  35 + ///自动进入下一环节
  36 + final bool autoNextSection;
  37 + RequestEndClassEvent(this.courseLessonId,this.currentStep,this.currentTime,{this.autoNextSection = false});
  38 +}
  39 +
30 40 ///页面切换
31 41 class CurrentUnitIndexChangeEvent extends SectionEvent {
32 42 final int unitIndex;
... ...
lib/pages/section/section_page.dart
... ... @@ -6,6 +6,7 @@ import &#39;package:nested_scroll_views/material.dart&#39;;
6 6 import 'package:wow_english/common/core/user_util.dart';
7 7 import 'package:wow_english/common/extension/string_extension.dart';
8 8 import 'package:wow_english/models/course_unit_entity.dart';
  9 +import 'package:wow_english/pages/section/section_type.dart';
9 10 import 'package:wow_english/pages/section/widgets/home_video_item.dart';
10 11 import 'package:wow_english/pages/section/widgets/section_bouns_item.dart';
11 12 import 'package:wow_english/pages/section/widgets/section_header_widget.dart';
... ... @@ -52,15 +53,15 @@ class _SectionPageView extends StatelessWidget {
52 53 if (state is RequestVideoLessonState) {
53 54 final videoUrl = bloc.processEntity?.videos?.videoUrl ?? '';
54 55 var title = '';
55   - if (state.type == 1) {
  56 + if (state.type == SectionType.song.value) {
56 57 title = 'song';
57 58 }
58 59  
59   - if (state.type == 2) {
  60 + if (state.type == SectionType.video.value) {
60 61 title = 'video';
61 62 }
62 63  
63   - if (state.type == 5) {
  64 + if (state.type == SectionType.bouns.value) {
64 65 title = 'bonus';
65 66 }
66 67  
... ... @@ -70,22 +71,21 @@ class _SectionPageView extends StatelessWidget {
70 71 pushNamed(AppRouteName.lookVideo, arguments: {
71 72 'videoUrl': videoUrl,
72 73 'title': title,
73   - 'courseLessonId': state.courseLessonId
  74 + 'courseLessonId': state.courseLessonId,
  75 + 'isTopic': true
74 76 }).then((value) {
75 77 if (value != null) {
76   - Map<String, String> dataMap = value as Map<String, String>;
77   - bloc.add(RequestExitClassEvent(
78   - dataMap['courseLessonId']!,
79   - '0',
80   - dataMap['currentTime']!,
81   - ));
  78 + Map<String, dynamic> dataMap = value as Map<String, dynamic>;
  79 + bloc.add(RequestEndClassEvent(dataMap['courseLessonId']!, '0',
  80 + dataMap['currentTime']!, autoNextSection: dataMap['nextSection'] as bool));
82 81 }
83 82 });
84 83 return;
85 84 }
86 85  
87 86 if (state is RequestEnterClassState) {
88   - if (state.courseType != 3 && state.courseType != 4) {
  87 + if (state.courseType != SectionType.practice.value
  88 + && state.courseType != SectionType.pictureBook.value) {
89 89 ///视频类型
90 90 ///获取视频课程内容
91 91 bloc.add(RequestVideoLessonEvent(
... ... @@ -93,7 +93,7 @@ class _SectionPageView extends StatelessWidget {
93 93 return;
94 94 }
95 95  
96   - if (state.courseType == 4) {
  96 + if (state.courseType == SectionType.pictureBook.value) {
97 97 //绘本
98 98 pushNamed(AppRouteName.reading,
99 99 arguments: {'courseLessonId': state.courseLessonId})
... ... @@ -107,7 +107,7 @@ class _SectionPageView extends StatelessWidget {
107 107 return;
108 108 }
109 109  
110   - if (state.courseType == 3) {
  110 + if (state.courseType == SectionType.practice.value) {
111 111 //练习
112 112 pushNamed(AppRouteName.topicPic,
113 113 arguments: {'courseLessonId': state.courseLessonId})
... ... @@ -214,7 +214,7 @@ Widget _itemTransCard(int index, BuildContext context) {
214 214 scrollDirection: Axis.horizontal,
215 215 itemBuilder: (BuildContext context, int index) {
216 216 CourseSectionEntity sectionData = bloc.courseSectionDatas![index];
217   - if (sectionData.courseType == 5) {
  217 + if (sectionData.courseType == SectionType.bouns.value) {
218 218 //彩蛋
219 219 return GestureDetector(
220 220 onTap: () {
... ...
lib/pages/section/section_type.dart 0 → 100644
  1 +///环节类型
  2 +enum SectionType {
  3 + ///儿歌
  4 + song,
  5 +
  6 + ///视频
  7 + video,
  8 +
  9 + ///练习
  10 + practice,
  11 +
  12 + ///绘本
  13 + pictureBook,
  14 +
  15 + ///彩蛋
  16 + bouns
  17 +}
  18 +
  19 +extension SectionTypeExtension on SectionType {
  20 + int get value {
  21 + switch (this) {
  22 + case SectionType.song:
  23 + return 1;
  24 + case SectionType.video:
  25 + return 2;
  26 + case SectionType.practice:
  27 + return 3;
  28 + case SectionType.pictureBook:
  29 + return 4;
  30 + case SectionType.bouns:
  31 + return 5;
  32 + default:
  33 + throw ArgumentError('Unknown section type');
  34 + }
  35 + }
  36 +}
... ...
lib/pages/section/subsection/base_section/bloc.dart 0 → 100644
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter_bloc/flutter_bloc.dart';
  3 +import 'package:wow_english/common/extension/string_extension.dart';
  4 +
  5 +import '../../../../route/route.dart';
  6 +import 'event.dart';
  7 +import 'state.dart';
  8 +
  9 +abstract class BaseSectionBloc<E extends BaseSectionEvent,
  10 + S extends BaseSectionState> extends Bloc<E, S> {
  11 + BaseSectionBloc(super.initialState);
  12 +
  13 + bool isCompleteDialogShow = false;
  14 +
  15 + // 这里可以定义一些通用的逻辑
  16 + void completeSection(final VoidCallback? nextSectionTap) {
  17 + // 逻辑来标记步骤为已完成
  18 + // 比如更新状态
  19 + if (isCompleteDialogShow) {
  20 + return;
  21 + }
  22 + isCompleteDialogShow = true;
  23 + showDialog(
  24 + context: AppRouter.context,
  25 + barrierDismissible: false,
  26 + barrierColor: Colors.black54,
  27 + builder: (BuildContext context) {
  28 + return AlertDialog(
  29 + backgroundColor: Colors.transparent,
  30 + content: SizedBox(
  31 + width: double.infinity, // 宽度设置为无限,使其尽可能铺满屏幕
  32 + height: MediaQuery.of(context).size.height * 0.6, // 高度设置为屏幕高度的60%
  33 + child: Row(
  34 + mainAxisAlignment: MainAxisAlignment.spaceBetween, // 图片之间分配空间
  35 + children: <Widget>[
  36 + // 左侧可点击的图片
  37 + Expanded(
  38 + flex: 1,
  39 + child: GestureDetector(
  40 + onTap: () {
  41 + isCompleteDialogShow = false;
  42 + popPage();
  43 + add(SectionAgainEvent() as E);
  44 + },
  45 + child: Image.asset('section_finish_again'.assetPng),
  46 + ),
  47 + ),
  48 + // 中间的图片
  49 + Expanded(
  50 + flex: 2,
  51 + child: Image.asset('section_finish_steve'.assetPng),
  52 + ),
  53 + // 右侧可点击的图片
  54 + Expanded(
  55 + flex: 1,
  56 + child: GestureDetector(
  57 + onTap: () {
  58 + // 处理右侧图片的点击事件
  59 + isCompleteDialogShow = false;
  60 + popPage();
  61 + nextSectionTap!();
  62 + },
  63 + child: Image.asset('section_finish_next'.assetPng),
  64 + ),
  65 + ),
  66 + ],
  67 + ),
  68 + ),
  69 + );
  70 + },
  71 + );
  72 + }
  73 +}
... ...
lib/pages/section/subsection/base_section/event.dart 0 → 100644
  1 +abstract class BaseSectionEvent {}
  2 +
  3 +///环节完成(结束)
  4 +class SectionCompleted extends BaseSectionEvent {}
  5 +
  6 +///环节再来一次
  7 +class SectionAgainEvent extends BaseSectionEvent {}
  8 +
  9 +///下一个环节
  10 +class SectionNextEvent extends BaseSectionEvent {}
0 11 \ No newline at end of file
... ...
lib/pages/section/subsection/base_section/state.dart 0 → 100644
  1 +abstract class BaseSectionState {}
  2 +
  3 +class SectionCompleted extends BaseSectionState {}
... ...
lib/pages/video/lookvideo/bloc/look_video_bloc.dart
1 1 import 'package:flutter/cupertino.dart';
2 2 import 'package:flutter_bloc/flutter_bloc.dart';
3 3 import 'package:video_player/video_player.dart';
  4 +import 'package:wow_english/pages/section/subsection/base_section/bloc.dart';
  5 +import 'package:wow_english/pages/section/subsection/base_section/event.dart';
  6 +import 'package:wow_english/pages/section/subsection/base_section/state.dart';
4 7  
5 8 part 'look_video_event.dart';
6 9 part 'look_video_state.dart';
7 10  
8   -class LookVideoBloc extends Bloc<LookVideoEvent, LookVideoState> {
  11 +class LookVideoBloc extends BaseSectionBloc<LookVideoEvent, LookVideoState> {
9 12  
10 13 VideoPlayerController? _controller;
11 14  
12   - LookVideoBloc() : super(LookVideoInitial()) {
  15 + final String? _videoUrl;
  16 + String? get videoUrl => _videoUrl;
  17 + final String? _typeTitle;
  18 + String? get typeTitle => _typeTitle;
  19 + final String? _courseLessonId;
  20 + String? get courseLessonId => _courseLessonId;
  21 + final bool _isTopic;
  22 + bool get isTopic => _isTopic;
  23 +
  24 + LookVideoBloc(this._videoUrl, this._typeTitle, this._courseLessonId, this._isTopic) : super(LookVideoInitial()) {
13 25 on<LookVideoEvent>((event, emit) {
14 26 // TODO: implement event handler
15 27 });
... ...
lib/pages/video/lookvideo/bloc/look_video_event.dart
1 1 part of 'look_video_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class LookVideoEvent {}
  4 +abstract class LookVideoEvent extends BaseSectionEvent {}
... ...
lib/pages/video/lookvideo/bloc/look_video_state.dart
1 1 part of 'look_video_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class LookVideoState {}
  4 +abstract class LookVideoState extends BaseSectionState {}
5 5  
6 6 class LookVideoInitial extends LookVideoState {}
7 7  
... ...
lib/pages/video/lookvideo/look_video_page.dart
1 1 import 'package:flutter/material.dart';
  2 +import 'package:flutter_bloc/flutter_bloc.dart';
  3 +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
2 4 import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart';
3 5  
4   -class LookVideoPage extends StatefulWidget {
5   - const LookVideoPage({super.key, this.videoUrl, this.typeTitle, this.courseLessonId});
  6 +class LookVideoPage extends StatelessWidget {
  7 + const LookVideoPage(
  8 + {super.key, this.videoUrl, this.typeTitle, this.courseLessonId, this.isTopic = false});
6 9  
7 10 final String? videoUrl;
8 11 final String? typeTitle;
9 12 final String? courseLessonId;
  13 + final bool isTopic;
10 14  
11 15 @override
12   - State<StatefulWidget> createState() {
13   - return _LookVideoPageState();
14   - }
15   -}
16   -
17   -class _LookVideoPageState extends State<LookVideoPage> {
18   - @override
19 16 Widget build(BuildContext context) {
20   - return Container(
21   - color: Colors.white,
22   - child: VideoWidget(
23   - videoUrl: widget.videoUrl??'',
24   - typeTitle: widget.typeTitle,
25   - courseLessonId: widget.courseLessonId??'',
26   - ),
  17 + return BlocProvider(
  18 + create: (BuildContext context) => LookVideoBloc(videoUrl, typeTitle, courseLessonId, isTopic),
  19 + child: Builder(builder: (context) => _buildPage(context)),
27 20 );
28 21 }
29   -}
30 22 \ No newline at end of file
  23 +}
  24 +
  25 +Widget _buildPage(BuildContext context) {
  26 + return BlocBuilder<LookVideoBloc, LookVideoState>(builder: (context, state) {
  27 + final bloc = BlocProvider.of<LookVideoBloc>(context);
  28 + return Container(
  29 + color: Colors.white,
  30 + child: VideoWidget(
  31 + videoUrl: bloc.videoUrl ?? '',
  32 + typeTitle: bloc.typeTitle ?? '',
  33 + courseLessonId: bloc.courseLessonId ?? '',
  34 + isTopic: bloc.isTopic,
  35 + )
  36 + );
  37 + }
  38 + );
  39 +}
... ...
lib/pages/video/lookvideo/widgets/video_widget.dart
1 1 import 'package:common_utils/common_utils.dart';
2 2 import 'package:flutter/material.dart';
  3 +import 'package:flutter_bloc/flutter_bloc.dart';
3 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 5 import 'package:video_player/video_player.dart';
5 6 import 'package:wow_english/common/extension/string_extension.dart';
  7 +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
6 8 import 'package:wow_english/route/route.dart';
7 9  
8 10 import 'video_opera_widget.dart';
9 11  
10 12 class VideoWidget extends StatefulWidget {
11   - const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = ''});
  13 + const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = '', this.isTopic = false});
12 14  
13 15 final String videoUrl;
14 16 final String? typeTitle;
15 17 final String courseLessonId;
  18 + final bool isTopic;
16 19  
17 20 @override
18 21 State<StatefulWidget> createState() {
... ... @@ -51,6 +54,12 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
51 54 _playDegree = 0.0;
52 55 }
53 56 });
  57 + } else if (_controller!.value.isCompleted) {
  58 + context.read<LookVideoBloc>().completeSection((){
  59 + String currentTime = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toString();
  60 + popPage(data:{'courseLessonId':widget.courseLessonId,'currentTime':currentTime,
  61 + 'nextSection':widget.isTopic});
  62 + } as VoidCallback);
54 63 }
55 64 }
56 65 });
... ... @@ -114,7 +123,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
114 123 setState(() {
115 124 _currentTime = formatDuration(_controller!.value.position);
116 125 _totalTime = formatDuration(_controller!.value.duration);
117   - _controller!.setLooping(true);
  126 + _controller!.setLooping(!widget.isTopic);
118 127 _controller!.setVolume(100);
119 128 _controller!.play();
120 129 });
... ...
lib/route/route.dart
... ... @@ -190,11 +190,14 @@ class AppRouter {
190 190 final title = (settings.arguments as Map)['title'] as String?;
191 191 final courseLessonId =
192 192 (settings.arguments as Map)['courseLessonId'] as String?;
  193 + ///是否是课程内的视频环节,用于播放结束判断要不要再来一次以及下一环节用
  194 + final isTopic = (settings.arguments as Map)['isTopic'] as bool? ?? false;
193 195 return CupertinoPageRoute(
194 196 builder: (_) => LookVideoPage(
195 197 videoUrl: videoUrl,
196 198 typeTitle: title,
197 199 courseLessonId: courseLessonId,
  200 + isTopic: isTopic,
198 201 ));
199 202 /*case AppRouteName.setPwd:
200 203 case AppRouteName.setPwd:
... ...
lib/utils/date_util.dart 0 → 100644
  1 +
  2 +///获取当前时间(单位:秒)
  3 +int getTimestampOfSecond() {
  4 + // 获取当前时间
  5 + DateTime now = DateTime.now();
  6 +
  7 + // 获取自Unix纪元以来的毫秒数
  8 + int milliseconds = now.millisecondsSinceEpoch;
  9 +
  10 + // 将毫秒数转换为秒
  11 + int seconds = milliseconds ~/ 1000;
  12 +
  13 + return seconds;
  14 +}
0 15 \ No newline at end of file
... ...
pubspec.yaml
... ... @@ -90,7 +90,7 @@ dependencies:
90 90 # 富文本插件 https://pub.dev/packages/extended_text
91 91 extended_text: ^11.0.1
92 92 # 视频播放 https://pub.dev/packages/video_player
93   - video_player: ^2.7.0
  93 + video_player: ^2.8.6
94 94 # UI适配 https://pub.dev/packages/responsive_framework
95 95 responsive_framework: ^1.0.0
96 96 # 音频播放 https://pub.dev/packages/audioplayers
... ...