Commit 11f396024b5c792686719dc170d6153d58a0195f

Authored by biao
2 parents 9575e671 69140da9

Merge branch 'feat-wqf-payment' into ios_umeng

Showing 40 changed files with 1070 additions and 328 deletions
android/app/proguard-rules.pro
... ... @@ -24,4 +24,22 @@
24 24 -keep class com.tt.** { *; }
25 25 -keep class com.xs.** { *; }
26 26 -keep interface com.xs.** { *; }
27   --keep enum com.xs.** { *; }
28 27 \ No newline at end of file
  28 +-keep enum com.xs.** { *; }
  29 +
  30 +# 友盟混淆
  31 +-keep class com.umeng.** { *; }
  32 +
  33 +-keep class com.uc.** { *; }
  34 +
  35 +-keep class com.efs.** { *; }
  36 +
  37 +-keepclassmembers class *{
  38 + public<init>(org.json.JSONObject);
  39 +}
  40 +-keepclassmembers enum *{
  41 + public static **[] values();
  42 + public static ** valueOf(java.lang.String);
  43 +}
  44 +-keep public class com.kouyuxingqiu.wow_english.R$*{
  45 + public static final int *;
  46 +}
29 47 \ No newline at end of file
... ...
android/app/src/main/AndroidManifest.xml
... ... @@ -24,6 +24,8 @@
24 24 <intent-filter>
25 25 <action android:name="android.intent.action.MAIN"/>
26 26 <category android:name="android.intent.category.LAUNCHER"/>
  27 + <!-- 友盟apm集成测试用 -->
  28 +<!-- <data android:scheme="um.663b66b0b3362515012f4ea5" />-->
27 29 </intent-filter>
28 30 </activity>
29 31 <!-- Don't delete the meta-data below.
... ...
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt
... ... @@ -12,6 +12,8 @@ import com.kouyuxingqiu.wow_english.methodChannels.GameMethodChannel
12 12 import com.kouyuxingqiu.wow_english.methodChannels.SingSoungMethodChannel
13 13 import com.umeng.commonsdk.UMConfigure
14 14 import io.flutter.embedding.android.FlutterActivity
  15 +import com.umeng.umcrash.UMCrash
  16 +
15 17  
16 18 class MainActivity : FlutterActivity() {
17 19 override fun onCreate(savedInstanceState: Bundle?) {
... ... @@ -23,9 +25,7 @@ class MainActivity : FlutterActivity() {
23 25 SingSoungMethodChannel(this, it)
24 26 GameMethodChannel(this, it)
25 27 }
26   - //UM日志打印
27   - UMConfigure.setLogEnabled(true)
28   - UMConfigure.preInit(this, "663b66b0b3362515012f4ea5", "official")
  28 + initUmeng()
29 29 }
30 30  
31 31 override fun onResume() {
... ... @@ -56,4 +56,27 @@ class MainActivity : FlutterActivity() {
56 56 // 打开沉浸式
57 57 WindowCompat.setDecorFitsSystemWindows(window, false)*/
58 58 }
  59 +
  60 + /**
  61 + * 友盟初始化
  62 + */
  63 + private fun initUmeng() {
  64 + // 友盟集成测试阶段获取UMID
  65 +// android.util.Log.d("WQF UMConfigure", UMConfigure.getUMIDString(this))
  66 + // 在application.onCreate内配置各模块开关并预初始化SDK
  67 + // 重点关注:如果您还想采集Native 崩溃、ANR等日志可以参考下面设置
  68 + UMCrash.initConfig(Bundle().apply {
  69 + putBoolean(UMCrash.KEY_ENABLE_CRASH_JAVA, true)
  70 + putBoolean(UMCrash.KEY_ENABLE_CRASH_NATIVE, true)
  71 + putBoolean(UMCrash.KEY_ENABLE_ANR, true)
  72 + putBoolean(UMCrash.KEY_ENABLE_PA, false)
  73 + putBoolean(UMCrash.KEY_ENABLE_LAUNCH, false)
  74 + putBoolean(UMCrash.KEY_ENABLE_MEM, false)
  75 + putBoolean(UMCrash.KEY_ENABLE_H5PAGE, false)
  76 + putBoolean(UMCrash.KEY_ENABLE_POWER, false)
  77 + })
  78 + //UM日志打印
  79 + UMConfigure.setLogEnabled(false)
  80 + UMConfigure.preInit(this, "663b66b0b3362515012f4ea5", "official")
  81 + }
59 82 }
... ...
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

build.yaml 0 → 100644
  1 +#targets:
  2 +# $default:
  3 +# builders:
  4 +# json_serializable:
  5 +# options:
  6 +# # 指定生成代码的配置
  7 +# # 是否要在生成的代码中包含 fromJson 和 toJson 方法的方法签名
  8 +# # 默认情况下,此值为false,设置为true可以生成方法签名,便于调试
  9 +# generate_to_json: true
  10 +# generate_from_json: true
... ...
lib/app/app.dart
... ... @@ -3,6 +3,7 @@ import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;
3 3 import 'package:flutter_easyloading/flutter_easyloading.dart';
4 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
5 5 import 'package:responsive_framework/responsive_framework.dart';
  6 +import 'package:umeng_apm_sdk/umeng_apm_sdk.dart';
6 7 import 'package:wow_english/common/blocs/cachebloc/cache_bloc.dart';
7 8 import 'package:wow_english/common/widgets/hide_keyboard_widget.dart';
8 9 import 'package:wow_english/pages/tab/blocs/tab_bloc.dart';
... ... @@ -10,7 +11,9 @@ import &#39;package:wow_english/pages/user/bloc/user_bloc.dart&#39;;
10 11 import 'package:wow_english/route/route.dart';
11 12  
12 13 class App extends StatelessWidget {
13   - const App({super.key});
  14 + const App([this._navigatorObserver]);
  15 +
  16 + final NavigatorObserver? _navigatorObserver;
14 17  
15 18 @override
16 19 Widget build(BuildContext context) {
... ... @@ -41,6 +44,11 @@ class App extends StatelessWidget {
41 44 initialRoute: AppRouteName.splash,
42 45 navigatorKey: AppRouter.navigatorKey,
43 46 onGenerateRoute: AppRouter.generateRoute,
  47 + navigatorObservers: <NavigatorObserver>[
  48 + // 带入ApmNavigatorObserver实例用于路由监听
  49 + // 如果不带入SDK监听器将无法获知页面(PV)入栈退栈行为,错误率(Dart异常数/FlutterPV次数)将异常攀升。
  50 + _navigatorObserver ?? ApmNavigatorObserver.singleInstance
  51 + ],
44 52 ),
45 53 )),
46 54 );
... ...
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/basic_config.dart
1   -import 'package:flutter/cupertino.dart';
  1 +import 'package:flutter/foundation.dart';
2 2  
3 3 class BasicConfig {
4 4 // static bool isTestDev = true;
... ... @@ -7,7 +7,6 @@ class BasicConfig {
7 7  
8 8 // 暂时未启用
9 9 static bool isEnvProd() {
10   - bool kReleaseMode = const bool.fromEnvironment('dart.vm.product');
11 10 if (kReleaseMode) {
12 11 debugPrint("dart.vm.product-现在是release环境.");
13 12 } else {
... ...
lib/common/request/dao/listen_dao.dart
... ... @@ -4,6 +4,7 @@ import &#39;package:wow_english/models/follow_read_entity.dart&#39;;
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 /// 磨耳朵
... ... @@ -14,37 +15,66 @@ class ListenDao {
14 15  
15 16 ///视频跟读
16 17 static Future<List<FollowReadEntity?>?> followRead() async {
17   - var data = await requestClient.get<List<FollowReadEntity?>>(Apis.followRead);
  18 + var data =
  19 + await requestClient.get<List<FollowReadEntity?>>(Apis.followRead);
18 20 return data;
19 21 }
20 22  
21 23 ///课程内容
22 24 static Future<CourseProcessEntity?> process(courseLessonId) async {
23   - var data = await requestClient.get<CourseProcessEntity>(Apis.process,queryParameters: {'courseLessonId':courseLessonId});
  25 + var data = await requestClient.get<CourseProcessEntity>(Apis.process,
  26 + queryParameters: {'courseLessonId': courseLessonId});
24 27 return data;
25 28 }
26 29  
27 30 ///获取视频跟读内容
28   - static Future<List<ReadContentEntity?>?> readContent(videoFollowReadId) async {
29   - var data = await requestClient.get<List<ReadContentEntity?>>(Apis.readContent,queryParameters: {'videoFollowReadId':videoFollowReadId});
  31 + static Future<List<ReadContentEntity?>?> readContent(
  32 + videoFollowReadId) async {
  33 + var data = await requestClient.get<List<ReadContentEntity?>>(
  34 + Apis.readContent,
  35 + queryParameters: {'videoFollowReadId': videoFollowReadId});
30 36 return data;
31 37 }
32 38  
33 39 ///视频跟读提交结果
34   - static Future followResult(frequency,videoFollowReadId) async {
35   - var data = await requestClient.post(Apis.followResult,data: {'frequency':frequency,'videoFollowReadContentId':videoFollowReadId});
  40 + static Future followResult(frequency, videoFollowReadId) async {
  41 + var data = await requestClient.post(Apis.followResult, data: {
  42 + 'frequency': frequency,
  43 + 'videoFollowReadContentId': videoFollowReadId
  44 + });
36 45 return data;
37 46 }
38 47  
39 48 ///进入课堂
40 49 static Future enterClass(courseLessonId) async {
41   - var data = await requestClient.post(Apis.enterClass,data: {'courseLessonId':courseLessonId});
  50 + var data = await requestClient
  51 + .post(Apis.enterClass, data: {'courseLessonId': courseLessonId});
42 52 return data;
43 53 }
44 54  
45 55 ///退出课堂
46   - static Future exitClass(courseLessonId,currentStep,currentTime) async {
47   - var data = await requestClient.post(Apis.exitClass,data: {'courseLessonId':courseLessonId,'currentStep':currentStep,'currentTime':currentTime});
  56 + static Future exitClass(courseLessonId,
  57 + {int? currentStep, int? currentTime}) async {
  58 + var data = await requestClient.post(Apis.exitClass, data: {
  59 + 'courseLessonId': courseLessonId,
  60 +
  61 + ///如果currentStep不为空,才传currentStep参数
  62 + if (currentStep != null) 'currentStep': currentStep,
  63 +
  64 + ///如果currentTime不为空,才传currentTime参数
  65 + if (currentTime != null) 'currentTime': currentTime
  66 + });
  67 + return data;
  68 + }
  69 +
  70 + ///完成课堂
  71 + static Future endClass(courseLessonId,
  72 + {int? currentStep, int? currentTime}) async {
  73 + var data = await requestClient.post(Apis.endClass, data: {
  74 + 'courseLessonId': courseLessonId,
  75 + if (currentStep != null) 'currentStep': currentStep,
  76 + if (currentTime != null) 'currentTime': currentTime
  77 + });
48 78 return data;
49 79 }
50 80 }
... ...
lib/main.dart
1 1 import 'dart:io';
2 2  
  3 +import 'package:flutter/foundation.dart';
3 4 import 'package:flutter/material.dart';
4 5 import 'package:flutter/services.dart';
  6 +import 'package:package_info_plus/package_info_plus.dart';
  7 +import 'package:umeng_apm_sdk/umeng_apm_sdk.dart';
5 8 import 'package:wow_english/app/app.dart';
6 9  
  10 +// void main() {
  11 +// ///设置设备默认方向
  12 +// WidgetsFlutterBinding.ensureInitialized();
  13 +// if (Platform.isAndroid) {
  14 +// SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( //设置状态栏透明
  15 +// statusBarColor: Colors.transparent,
  16 +// ));
  17 +// }
  18 +// runApp(const App());
  19 +// }
  20 +
7 21 void main() {
8 22 ///设置设备默认方向
9   - WidgetsFlutterBinding.ensureInitialized();
10 23 if (Platform.isAndroid) {
11 24 SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( //设置状态栏透明
12 25 statusBarColor: Colors.transparent,
13 26 ));
14 27 }
15   - runApp(const App());
  28 +
  29 + final UmengApmSdk umengApmSdk = UmengApmSdk(
  30 + name: "",
  31 + bver: "",
  32 +
  33 + // 是否开启SDK运行时日志输出
  34 + enableLog: !kReleaseMode,
  35 +
  36 + // 您使用的flutter版本,默认为空,为方便定位访问,建议配置
  37 + flutterVersion: '3.13.2',
  38 +
  39 + // 您使用的flutter引擎版本
  40 + engineVersion: 'ff5b5b5fa6',
  41 +
  42 + // 开启监测页面帧率(默认关闭) 版本 v2.1.3 可支持
  43 + enableTrackingPageFps: !kReleaseMode,
  44 +
  45 + // 开启监测页面性能(默认关闭)版本 v2.1.3 可支持
  46 + enableTrackingPagePerf: !kReleaseMode,
  47 +
  48 + // 带入继承ApmWidgetsFlutterBinding的覆写和初始化方法, 可用于自定义监听应用生命周期
  49 + // 确保去掉原有的WidgetsFlutterBinding.ensureInitialized() ,以免出现重复初始化绑定的异常造成无法正常初始化,SDK内部已通过initFlutterBinding入参带入继承的WidgetsFlutterBinding实现初始化操作
  50 + initFlutterBinding: MyApmWidgetsFlutterBinding.ensureInitialized,
  51 +
  52 + // 抛出异常事件
  53 + onError: (exception, stack) {
  54 + debugPrint(exception.toString());
  55 + },
  56 +
  57 + );
  58 + umengApmSdk.init(appRunner: (observer) async {
  59 + // 确保去掉原有的WidgetsFlutterBinding.ensureInitialized() ,以免出现重复初始化绑定的异常造成无法正常初始化,SDK内部已通过initFlutterBinding入参带入继承的WidgetsFlutterBinding实现初始化操作
  60 + // 依赖ensureInitialized()初始化的代码可在此调用
  61 + // 需要异步获取设置应用名称和版本号可在此回调中操作
  62 + // SDK实例化的设置可先将name和bver 为 "",然后通过以下方式进行设置
  63 + PackageInfo packageInfo = await PackageInfo.fromPlatform();
  64 + String packageName = packageInfo.packageName;
  65 + String buildNumber = packageInfo.buildNumber;
  66 + String version = packageInfo.version;
  67 + umengApmSdk.name = packageName;
  68 + umengApmSdk.bver = '$version+$buildNumber';
  69 +
  70 + return App(observer);
  71 + });
  72 +}
  73 +
  74 +
  75 +//ApmWidgetsFlutterBinding的覆写和初始化方法
  76 +class MyApmWidgetsFlutterBinding extends ApmWidgetsFlutterBinding {
  77 + @override
  78 + void handleAppLifecycleStateChanged(AppLifecycleState state) {
  79 + // 添加自己的实现逻辑
  80 + debugPrint('AppLifecycleState changed to $state');
  81 + super.handleAppLifecycleStateChanged(state);
  82 + }
  83 +
  84 + static WidgetsBinding? ensureInitialized() {
  85 + MyApmWidgetsFlutterBinding();
  86 + return WidgetsBinding.instance;
  87 + }
16 88 }
... ...
lib/pages/practice/bloc/topic_picture_bloc.dart
... ... @@ -9,10 +9,14 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;;
9 9 import 'package:wow_english/common/request/dao/listen_dao.dart';
10 10 import 'package:wow_english/common/request/exception.dart';
11 11 import 'package:wow_english/models/course_process_entity.dart';
  12 +import 'package:wow_english/pages/section/subsection/base_section/bloc.dart';
  13 +import 'package:wow_english/pages/section/subsection/base_section/event.dart';
  14 +import 'package:wow_english/pages/section/subsection/base_section/state.dart';
12 15 import 'package:wow_english/utils/loading.dart';
13 16 import 'package:wow_english/utils/toast_util.dart';
14 17  
15 18 import '../../../common/permission/permissionRequestPage.dart';
  19 +import '../../../route/route.dart';
16 20  
17 21 part 'topic_picture_event.dart';
18 22 part 'topic_picture_state.dart';
... ... @@ -28,7 +32,7 @@ enum VoicePlayState {
28 32 stop
29 33 }
30 34  
31   -class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> {
  35 +class TopicPictureBloc extends BaseSectionBloc<TopicPictureEvent, TopicPictureState> {
32 36  
33 37 final PageController pageController;
34 38  
... ... @@ -94,11 +98,15 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
94 98 _forbiddenWhenCorrect = false;
95 99 debugPrint('播放完成后解除禁止');
96 100 if (event == PlayerState.completed) {
97   - // 答对后且播放完自动翻页
98   - pageController.nextPage(
99   - duration: const Duration(milliseconds: 500),
100   - curve: Curves.ease,
101   - );
  101 + if (isLastPage()) {
  102 + showStepPage();
  103 + } else {
  104 + // 答对后且播放完自动翻页
  105 + pageController.nextPage(
  106 + duration: const Duration(milliseconds: 500),
  107 + curve: Curves.ease,
  108 + );
  109 + }
102 110 }
103 111 }
104 112 }
... ... @@ -263,6 +271,9 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
263 271 showToast('测评成功,分数是$overall',duration: const Duration(seconds: 5));
264 272 _isVoicing = false;
265 273 emitter(XSVoiceTestState());
  274 + if (isLastPage()) {
  275 + showStepPage();
  276 + }
266 277 }
267 278  
268 279 // 暂时没用上
... ... @@ -288,6 +299,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
288 299 }
289 300 }
290 301  
  302 + ///播放选择结果音效
291 303 void _playResultSound(bool isCorrect) async {
292 304 // await audioPlayer.stop();
293 305 if (audioPlayer.state == PlayerState.playing && _isResultSoundPlaying == false) {
... ... @@ -302,4 +314,26 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
302 314 await audioPlayer.play(AssetSource('incorrect_voice'.assetMp3));
303 315 }
304 316 }
  317 +
  318 + ///是否是最后一页
  319 + bool isLastPage() {
  320 + return currentPage == _entity?.topics?.length;
  321 + }
  322 +
  323 + ///展示过渡页
  324 + void showStepPage() {
  325 + ///如果最后一页是语音问答题,评测完后自动翻页
  326 + sectionComplete(() {
  327 + popPage(
  328 + data:{
  329 + 'currentStep':currentPage,
  330 + 'courseLessonId':courseLessonId,
  331 + 'isCompleted': true,
  332 + 'nextSection': true
  333 + });
  334 + }, againSectionTap: () {
  335 + debugPrint("WQF 重做");
  336 + pageController.jumpToPage(0);
  337 + });
  338 + }
305 339 }
... ...
lib/pages/practice/bloc/topic_picture_event.dart
1 1 part of 'topic_picture_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class TopicPictureEvent {}
  4 +abstract class TopicPictureEvent extends BaseSectionEvent {}
5 5  
6 6 class InitBlocEvent extends TopicPictureEvent {}
7 7  
... ...
lib/pages/practice/bloc/topic_picture_state.dart
1 1 part of 'topic_picture_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class TopicPictureState {}
  4 +abstract class TopicPictureState extends BaseSectionState {}
5 5  
6 6 class TopicPictureInitial extends TopicPictureState {}
7 7  
... ...
lib/pages/practice/topic_picture_page.dart
... ... @@ -6,6 +6,7 @@ import &#39;package:wow_english/common/core/user_util.dart&#39;;
6 6 import 'package:wow_english/common/extension/string_extension.dart';
7 7 import 'package:wow_english/common/widgets/ow_image_widget.dart';
8 8 import 'package:wow_english/models/course_process_entity.dart';
  9 +import 'package:wow_english/pages/practice/topic_type.dart';
9 10 import 'package:wow_english/route/route.dart';
10 11 import 'package:wow_english/utils/toast_util.dart';
11 12  
... ... @@ -70,8 +71,9 @@ class _TopicPicturePage extends StatelessWidget {
70 71 onTap: () {
71 72 popPage(
72 73 data:{
73   - 'currentStep':bloc.currentPage.toString(),
74   - 'courseLessonId':bloc.courseLessonId
  74 + 'currentStep':bloc.currentPage,
  75 + 'courseLessonId':bloc.courseLessonId,
  76 + 'isCompleted': bloc.isLastPage(),
75 77 });
76 78 // Navigator.pop(context);
77 79 },
... ... @@ -86,13 +88,13 @@ class _TopicPicturePage extends StatelessWidget {
86 88 },
87 89 itemBuilder: (BuildContext context,int index){
88 90 CourseProcessTopics? topics = bloc.entity?.topics![index];
89   - if (topics?.type == 1) {//听音选图
  91 + if (topics?.type == TopicType.audioImageSelect.value) {//听音选图
90 92 return _pageViewVoicePictureItemWidget(topics);
91   - } else if (topics?.type == 2) {//听音选字
  93 + } else if (topics?.type == TopicType.audioCharSelect.value) {//听音选字
92 94 return _pageViewVoiceWordItemWidget(topics);
93   - } else if (topics?.type == 3) {//看题选字
  95 + } else if (topics?.type == TopicType.questionCharSelect.value) {//看题选字
94 96 return _pageViewWordItemWidget(topics);
95   - } else if (topics?.type == 4) {//看题选图
  97 + } else if (topics?.type == TopicType.questionImageSelect.value) {//看题选图
96 98 return _pageViewItemWidget(topics);
97 99 } else {//语音问答
98 100 return _voiceAnswerItem(topics);
... ...
lib/pages/practice/topic_type.dart 0 → 100644
  1 +///练习(题型)类型
  2 +enum TopicType {
  3 + ///听音选图
  4 + audioImageSelect,
  5 +
  6 + ///听音选字
  7 + audioCharSelect,
  8 +
  9 + ///看题选字
  10 + questionCharSelect,
  11 +
  12 + ///看题选图
  13 + questionImageSelect,
  14 +
  15 + ///语音问答
  16 + voiceQuestion
  17 +}
  18 +
  19 +extension TopicTypeExtension on TopicType {
  20 + int get value {
  21 + switch (this) {
  22 + case TopicType.audioImageSelect:
  23 + return 1;
  24 + case TopicType.audioCharSelect:
  25 + return 2;
  26 + case TopicType.questionCharSelect:
  27 + return 3;
  28 + case TopicType.questionImageSelect:
  29 + return 4;
  30 + case TopicType.voiceQuestion:
  31 + return 5;
  32 + default:
  33 + throw ArgumentError('Unknown topic type');
  34 + }
  35 + }
  36 +}
... ...
lib/pages/reading/bloc/reading_bloc.dart
... ... @@ -6,11 +6,15 @@ import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;
6 6 import 'package:flutter_easyloading/flutter_easyloading.dart';
7 7 import 'package:permission_handler/permission_handler.dart';
8 8 import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart';
  9 +import 'package:wow_english/pages/section/subsection/base_section/bloc.dart';
  10 +import 'package:wow_english/pages/section/subsection/base_section/event.dart';
  11 +import 'package:wow_english/pages/section/subsection/base_section/state.dart';
9 12  
10 13 import '../../../common/core/user_util.dart';
11 14 import '../../../common/request/dao/listen_dao.dart';
12 15 import '../../../common/request/exception.dart';
13 16 import '../../../models/course_process_entity.dart';
  17 +import '../../../route/route.dart';
14 18 import '../../../utils/loading.dart';
15 19  
16 20 import '../../../utils/log_util.dart';
... ... @@ -33,7 +37,8 @@ enum VoicePlayState {
33 37 stop
34 38 }
35 39  
36   -class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> {
  40 +class ReadingPageBloc
  41 + extends BaseSectionBloc<ReadingPageEvent, ReadingPageState> {
37 42 final PageController pageController;
38 43  
39 44 final String courseLessonId;
... ... @@ -211,7 +216,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
211 216 }
212 217  
213 218 ///播放原音音频
214   - Future<void> _playOriginalAudioInner(String? audioUrl) async {
  219 + void _playOriginalAudioInner(String? audioUrl) async {
215 220 if (_isRecordAudioPlaying) {
216 221 _isRecordAudioPlaying = false;
217 222 }
... ... @@ -219,7 +224,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
219 224 "_playOriginalAudio _isRecordAudioPlaying=$_isRecordAudioPlaying _isOriginAudioPlaying=$_isOriginAudioPlaying url=$audioUrl");
220 225 if (_isOriginAudioPlaying) {
221 226 _isOriginAudioPlaying = false;
222   - await audioPlayer.stop();
  227 + audioPlayer.stop();
223 228 } else {
224 229 _isOriginAudioPlaying = true;
225 230 audioUrl ??= currentPageData()?.audioUrl ?? '';
... ... @@ -265,8 +270,17 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
265 270 }
266 271  
267 272 void nextPage() {
268   - if (_currentPage >= dataCount() - 1) {
269   - ///todo 最后一页了
  273 + if (currentPage >= dataCount()) {
  274 + sectionComplete(() {
  275 + popPage(data: {
  276 + 'currentStep': currentPage,
  277 + 'courseLessonId': courseLessonId,
  278 + 'isCompleted': true,
  279 + 'nextSection': true
  280 + });
  281 + }, againSectionTap: () {
  282 + pageController.jumpToPage(0);
  283 + });
270 284 } else {
271 285 _currentPage += 1;
272 286 pageController.nextPage(
... ... @@ -291,14 +305,14 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
291 305  
292 306 void startRecord(String content) async {
293 307 // 调用封装好的权限检查和请求方法
294   - bool result = await permissionCheckAndRequest(
295   - context,
296   - Permission.microphone,
297   - "录音"
298   - );
  308 + bool result =
  309 + await permissionCheckAndRequest(context, Permission.microphone, "录音");
299 310 if (result) {
300   - methodChannel.invokeMethod(
301   - 'startVoice', {'word': content, 'type': '0', 'userId': UserUtil.getUser()?.id.toString()});
  311 + methodChannel.invokeMethod('startVoice', {
  312 + 'word': content,
  313 + 'type': '0',
  314 + 'userId': UserUtil.getUser()?.id.toString()
  315 + });
302 316 }
303 317 }
304 318  
... ... @@ -313,7 +327,20 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
313 327 currentPageData()?.recordScore = overall;
314 328 currentPageData()?.recordUrl = args['audioUrl'] + '.mp3';
315 329 ///完成录音后紧接着播放录音
316   - _playRecordAudioInner();
  330 + await _playRecordAudioInner();
  331 + if (isLastPage()) {
  332 + sectionComplete(() {
  333 + popPage(data: {
  334 + 'currentStep': currentPage,
  335 + 'courseLessonId': courseLessonId,
  336 + 'isCompleted': true,
  337 + 'nextSection': true
  338 + });
  339 + }, againSectionTap: () {
  340 + _resetLocalResult();
  341 + pageController.jumpToPage(0);
  342 + });
  343 + }
317 344 // emitter(FeedbackState());
318 345 }
319 346  
... ... @@ -338,8 +365,11 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
338 365 nextPage();
339 366 }
340 367  
341   - Log.d("_onAudioPlayComplete _isOriginAudioPlaying=$_isOriginAudioPlaying _voicePlayState=$_voicePlayState recordUrl=${currentPageData()?.recordUrl?.isNotEmpty}");
342   - if (_isOriginAudioPlaying && _voicePlayState == VoicePlayState.completed && currentPageData()?.recordUrl?.isNotEmpty != true) {
  368 + Log.d(
  369 + "_onAudioPlayComplete _isOriginAudioPlaying=$_isOriginAudioPlaying _voicePlayState=$_voicePlayState recordUrl=${currentPageData()?.recordUrl?.isNotEmpty}");
  370 + if (_isOriginAudioPlaying &&
  371 + _voicePlayState == VoicePlayState.completed &&
  372 + currentPageData()?.recordUrl?.isNotEmpty != true) {
343 373 ///如果刚刚完成原音播放&&录音为空,则开始录音
344 374 startRecord(currentPageData()?.word ?? '');
345 375 }
... ... @@ -354,11 +384,21 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
354 384 _isRecordAudioPlaying = false;
355 385 }
356 386  
357   - void _onVoiceXsStateChange(
358   - OnXSVoiceStateChangeEvent event,
359   - Emitter<ReadingPageState> emitter
360   - ) async {
  387 + void _onVoiceXsStateChange(OnXSVoiceStateChangeEvent event,
  388 + Emitter<ReadingPageState> emitter) async {
361 389 emitter(XSVoiceTestState());
362 390 }
363   -}
364 391  
  392 + ///是否是最后一页
  393 + bool isLastPage() {
  394 + return currentPage == dataCount();
  395 + }
  396 +
  397 + ///重置数据
  398 + void _resetLocalResult() {
  399 + _entity?.readings?.forEach((element) {
  400 + element.recordScore = null;
  401 + element.recordUrl = null;
  402 + });
  403 + }
  404 +}
... ...
lib/pages/reading/bloc/reading_event.dart
1 1 part of 'reading_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class ReadingPageEvent {}
  4 +abstract class ReadingPageEvent extends BaseSectionEvent {}
5 5  
6 6 ///页面初始化
7 7 class InitBlocEvent extends ReadingPageEvent {}
... ...
lib/pages/reading/bloc/reading_state.dart
1 1 part of 'reading_bloc.dart';
2 2  
3 3 @immutable
4   -abstract class ReadingPageState {}
  4 +abstract class ReadingPageState extends BaseSectionState {}
5 5  
6 6 class ReadingPageInitial extends ReadingPageState {}
7 7  
... ...
lib/pages/reading/reading_page.dart
... ... @@ -88,8 +88,9 @@ class _ReadingPage extends StatelessWidget {
88 88 onPressed: () {
89 89 popPage(
90 90 data:{
91   - 'currentStep':bloc.currentPage.toString(),
92   - 'courseLessonId':bloc.courseLessonId
  91 + 'currentStep':bloc.currentPage,
  92 + 'courseLessonId':bloc.courseLessonId,
  93 + 'isCompleted':bloc.isLastPage(),
93 94 }
94 95 );
95 96 },
... ...
lib/pages/repeataftercontent/widgets/repeat_video_widget.dart
... ... @@ -140,13 +140,14 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
140 140 :
141 141 Container(
142 142 color: Colors.white,
143   - child: Text(
144   - '视频加载中....',
145   - style: TextStyle(
146   - fontSize: 20.sp,
147   - color: Colors.black
148   - ),
149   - ),
  143 + child: const CircularProgressIndicator(),
  144 + // Text(
  145 + // '视频加载中....',
  146 + // style: TextStyle(
  147 + // fontSize: 20.sp,
  148 + // color: Colors.black
  149 + // ),
  150 + // ),
150 151 ),
151 152 );
152 153 }
... ...
lib/pages/section/bloc/section_bloc.dart
... ... @@ -10,39 +10,55 @@ import &#39;package:wow_english/utils/toast_util.dart&#39;;
10 10  
11 11 import '../../../models/course_section_entity.dart';
12 12 import '../../../models/course_unit_entity.dart';
  13 +import '../../../utils/list_ext.dart';
13 14  
14 15 part 'section_event.dart';
15 16 part 'section_state.dart';
16 17  
17 18 class SectionBloc extends Bloc<SectionEvent, SectionState> {
18 19  
19   - CourseUnitEntity _courseUnitEntity;
  20 + PageController _pageController;
20 21  
21   - CourseUnitEntity get courseUnitEntity => _courseUnitEntity;
  22 + PageController get pageController => _pageController;
22 23  
23   - CourseUnitDetail _courseUnitDetail;
  24 + ///当前页索引
  25 + int _currentPage = 0;
24 26  
25   - CourseUnitDetail get courseUnitDetail => _courseUnitDetail;
  27 + int get currentPage => _currentPage;
26 28  
27   - List<CourseSectionEntity>? _courseSectionDatas;
  29 + ScrollController _listController;
28 30  
29   - List<CourseSectionEntity>? get courseSectionDatas => _courseSectionDatas;
  31 + ScrollController get listController => _listController;
  32 +
  33 + CourseUnitEntity _courseUnitEntity;
  34 +
  35 + CourseUnitEntity get courseUnitEntity => _courseUnitEntity;
  36 +
  37 + ///courseUnitId与课程环节列表的映射
  38 + final Map<int, List<CourseSectionEntity>?> _courseSectionDatasMap = {};
  39 +
  40 + Map<int, List<CourseSectionEntity>?> get courseSectionDatasMap => _courseSectionDatasMap;
30 41  
31 42 CourseProcessEntity? _processEntity;
32 43  
33 44 CourseProcessEntity? get processEntity => _processEntity;
34 45  
35   - SectionBloc(this._courseUnitEntity, this._courseUnitDetail) : super(LessonInitial()) {
36   - on<RequestDataEvent>(_requestData);
37   - on<RequestExitClassEvent>(_requestExitClass);
  46 + SectionBloc(this._courseUnitEntity, this._currentPage,
  47 + this._pageController, this._listController)
  48 + : super(LessonInitial()) {
  49 + on<RequestDataEvent>(_requestSectionsData);
  50 + on<RequestEndClassEvent>(_requestEndClass);
38 51 on<RequestEnterClassEvent>(_requestEnterClass);
39 52 on<RequestVideoLessonEvent>(_requestVideoLesson);
  53 + on<CurrentUnitIndexChangeEvent>(_pageControllerChange);
40 54 }
41 55  
42   - void _requestData(RequestDataEvent event, Emitter<SectionState> emitter) async {
  56 + void _requestSectionsData(
  57 + RequestDataEvent event, Emitter<SectionState> emitter) async {
43 58 try {
44 59 await loading(() async {
45   - _courseSectionDatas = await LessonDao.courseSection(courseUnitId: _courseUnitDetail.id!);
  60 + _courseSectionDatasMap[event.courseUnitId] =
  61 + await LessonDao.courseSection(courseUnitId: event.courseUnitId);
46 62 emitter(LessonDataLoadState());
47 63 });
48 64 } catch (e) {
... ... @@ -52,34 +68,156 @@ class SectionBloc extends Bloc&lt;SectionEvent, SectionState&gt; {
52 68 }
53 69 }
54 70  
55   - void _requestVideoLesson(RequestVideoLessonEvent event, Emitter<SectionState> emitter) async {
  71 + void _requestVideoLesson(
  72 + RequestVideoLessonEvent event, Emitter<SectionState> emitter) async {
56 73 try {
57 74 await loading(() async {
58 75 _processEntity = await ListenDao.process(event.courseLessonId);
59   - emitter(RequestVideoLessonState(event.courseLessonId,event.courseType));
  76 + emitter(
  77 + RequestVideoLessonState(event.courseLessonId, event.courseType));
60 78 });
61 79 } catch (e) {
62 80 if (e is ApiException) {
63   - showToast(e.message??'请求失败,请检查网络连接');
  81 + showToast(e.message ?? '请求失败,请检查网络连接');
64 82 }
65 83 }
66 84 }
67 85  
68   -
69   - void _requestEnterClass(RequestEnterClassEvent event,Emitter<SectionState> emitter) async {
  86 + void _requestEnterClass(
  87 + RequestEnterClassEvent event, Emitter<SectionState> emitter) async {
70 88 try {
71 89 await loading(() async {
72   - await ListenDao.enterClass(event.courseLessonId);
73   - emitter(RequestEnterClassState(event.courseLessonId,event.courseType));
  90 + await ListenDao.enterClass(event.courseLessonId);
  91 + emitter(RequestEnterClassState(event.courseLessonId, event.courseType));
74 92 });
75 93 } catch (e) {
76 94 if (e is ApiException) {
77   - showToast(e.message??'请求失败,请检查网络连接');
  95 + showToast(e.message ?? '请求失败,请检查网络连接');
78 96 }
79 97 }
80 98 }
81 99  
82   - void _requestExitClass(RequestExitClassEvent event,Emitter<SectionState> emitter) async {
83   - await ListenDao.exitClass(event.courseLessonId,event.currentStep,event.currentTime);
  100 + void _requestEndClass(
  101 + RequestEndClassEvent event, Emitter<SectionState> emitter) async {
  102 + if (event.isCompleted) {
  103 + await await ListenDao.endClass(event.courseLessonId,
  104 + currentStep: event.currentStep,
  105 + currentTime: event.currentTime);
  106 + } else {
  107 + await await ListenDao.exitClass(event.courseLessonId,
  108 + currentStep: event.currentStep,
  109 + currentTime: event.currentTime);
  110 + }
  111 + if (event.autoNextSection) {
  112 + final nextCourseSection =
  113 + await getNextCourseSection(int.parse(event.courseLessonId));
  114 + if (nextCourseSection != null) {
  115 + ///进入课堂
  116 + add(RequestEnterClassEvent(nextCourseSection.id.toString(),
  117 + nextCourseSection.courseType));
  118 + }
  119 + }
  120 + }
  121 +
  122 + void _pageControllerChange(
  123 + CurrentUnitIndexChangeEvent event, Emitter<SectionState> emitter) async {
  124 + _currentPage = event.unitIndex;
  125 + emitter(CurrentPageIndexState());
  126 + }
  127 +
  128 + ///未锁定的页(单元)数
  129 + int unlockPageCount() {
  130 + return _courseUnitEntity.courseUnitVOList
  131 + ?.indexWhereOrNull((element) => element.lock == true) ??
  132 + 1;
  133 + }
  134 +
  135 + ///当前页的课程详情
  136 + CourseUnitDetail getCourseUnitDetail({int? pageIndex}) {
  137 + return _courseUnitEntity.courseUnitVOList![pageIndex ?? _currentPage];
  138 + }
  139 +
  140 + ///根据courseLessonId查找对应的courseSection
  141 + CourseSectionEntity? findCourseSectionById(int courseLessonId) {
  142 + for (var entry in courseSectionDatasMap.entries) {
  143 + var sectionList = entry.value;
  144 + if (sectionList != null) {
  145 + for (var section in sectionList) {
  146 + if (section.id == courseLessonId) {
  147 + return section;
  148 + }
  149 + }
  150 + }
  151 + }
  152 + return null;
  153 + }
  154 +
  155 + ///根据sortOrder查找对应的courseSection
  156 + CourseSectionEntity? findCourseSectionBySort(int sortOrder) {
  157 + for (var entry in courseSectionDatasMap.entries) {
  158 + var sectionList = entry.value;
  159 + if (sectionList != null) {
  160 + for (var section in sectionList) {
  161 + if (section.sortOrder == sortOrder) {
  162 + return section;
  163 + }
  164 + }
  165 + }
  166 + }
  167 + return null;
  168 + }
  169 +
  170 + ///根据courseLessonId查找对应的CourseUnitDetail
  171 + CourseUnitDetail? findCourseUnitDetailById(int courseLessonId) {
  172 + final curCourseSectionEntity = findCourseSectionById(courseLessonId);
  173 + if (curCourseSectionEntity != null) {
  174 + final curCourseUnitDetail = _courseUnitEntity.courseUnitVOList?.firstWhere((element) =>
  175 + element.id == curCourseSectionEntity.courseUnitId);
  176 + return curCourseUnitDetail;
  177 + }
  178 + return null;
  179 + }
  180 +
  181 + ///根据courseLessonId查找下一个courseSection
  182 + Future<CourseSectionEntity?> getNextCourseSection(int courseLessonId) async {
  183 + final curCourseSectionEntity = findCourseSectionById(courseLessonId);
  184 + final curSectionSort = curCourseSectionEntity?.sortOrder ?? 0;
  185 + final nextCourseSectionEntity = findCourseSectionBySort(curSectionSort + 1);
  186 + if (nextCourseSectionEntity != null) {
  187 + return nextCourseSectionEntity;
  188 + } else {
  189 + ///跨unit选lesson
  190 + final curCourseUnitDetail = findCourseUnitDetailById(courseLessonId);
  191 + if (curCourseUnitDetail != null) {
  192 + final nextCourseUnitDetail = _courseUnitEntity.courseUnitVOList
  193 + ?.firstWhere((element) => element.sortOrder == (curCourseUnitDetail.sortOrder! + 1));
  194 + if (nextCourseUnitDetail != null) {
  195 + final courseUnitId = nextCourseUnitDetail.id!;
  196 + try {
  197 + await loading(() async {
  198 + _courseSectionDatasMap[courseUnitId] =
  199 + await LessonDao.courseSection(courseUnitId: courseUnitId);
  200 + emit(LessonDataLoadState());
  201 + });
  202 + _pageController.nextPage(
  203 + duration: const Duration(milliseconds: 500),
  204 + curve: Curves.ease,
  205 + );
  206 + return _courseSectionDatasMap[courseUnitId]!.first;
  207 + } catch (e) {
  208 + if (e is ApiException) {
  209 + showToast(e.message.toString());
  210 + }
  211 + return null;
  212 + }
  213 + } else {
  214 + ///最后一个unit了
  215 + return null;
  216 + }
  217 + } else {
  218 + ///找不到对应的unitDetail,理论上不可能
  219 + return null;
  220 + }
  221 + }
84 222 }
85 223 }
... ...
lib/pages/section/bloc/section_event.dart
... ... @@ -3,12 +3,17 @@ part of &#39;section_bloc.dart&#39;;
3 3 @immutable
4 4 abstract class SectionEvent {}
5 5  
6   -class RequestDataEvent extends SectionEvent {}
  6 +class RequestDataEvent extends SectionEvent {
  7 + final int courseUnitId;
  8 +
  9 + RequestDataEvent(this.courseUnitId);
  10 +}
7 11  
8 12 ///获取视频课程内容
9 13 class RequestVideoLessonEvent extends SectionEvent {
10 14 final String courseLessonId;
11 15 final int courseType;
  16 +
12 17 RequestVideoLessonEvent(this.courseLessonId, this.courseType);
13 18 }
14 19  
... ... @@ -16,13 +21,35 @@ class RequestVideoLessonEvent extends SectionEvent {
16 21 class RequestEnterClassEvent extends SectionEvent {
17 22 final String courseLessonId;
18 23 final int courseType;
19   - RequestEnterClassEvent(this.courseLessonId,this.courseType);
  24 +
  25 + RequestEnterClassEvent(this.courseLessonId, this.courseType);
20 26 }
21 27  
22   -///退出课堂
23   -class RequestExitClassEvent extends SectionEvent {
  28 +///结束课堂
  29 +class RequestEndClassEvent extends SectionEvent {
24 30 final String courseLessonId;
25   - final String currentStep;
26   - final String currentTime;
27   - RequestExitClassEvent(this.courseLessonId,this.currentStep,this.currentTime);
  31 +
  32 + ///当前进展(进度类,比如练习、绘本)
  33 + final int? currentStep;
  34 +
  35 + ///当前时间(进度类,比如音视频)
  36 + final int? currentTime;
  37 +
  38 + ///课程环节是否完成(决定调结束接口还是退出接口)
  39 + final bool isCompleted;
  40 +
  41 + ///自动进入下一环节
  42 + final bool autoNextSection;
  43 +
  44 + RequestEndClassEvent(this.courseLessonId, isCompleted,
  45 + {this.currentStep, this.currentTime, autoNextSection})
  46 + : isCompleted = isCompleted ?? false,
  47 + autoNextSection = autoNextSection ?? false;
  48 +}
  49 +
  50 +///页面切换
  51 +class CurrentUnitIndexChangeEvent extends SectionEvent {
  52 + final int unitIndex;
  53 +
  54 + CurrentUnitIndexChangeEvent(this.unitIndex);
28 55 }
... ...
lib/pages/section/bloc/section_state.dart
... ... @@ -10,11 +10,15 @@ class LessonDataLoadState extends SectionState {}
10 10 class RequestVideoLessonState extends SectionState {
11 11 final String courseLessonId;
12 12 final int type;
13   - RequestVideoLessonState(this.courseLessonId,this.type);
  13 +
  14 + RequestVideoLessonState(this.courseLessonId, this.type);
14 15 }
15 16  
16   -class RequestEnterClassState extends SectionState{
  17 +class RequestEnterClassState extends SectionState {
17 18 final String courseLessonId;
18 19 final int courseType;
19   - RequestEnterClassState(this.courseLessonId,this.courseType);
  20 +
  21 + RequestEnterClassState(this.courseLessonId, this.courseType);
20 22 }
  23 +
  24 +class CurrentPageIndexState extends SectionState {}
... ...
lib/pages/section/section_page.dart
  1 +import 'package:flutter/cupertino.dart';
1 2 import 'package:flutter/material.dart';
2 3 import 'package:flutter_bloc/flutter_bloc.dart';
3 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
  5 +import 'package:nested_scroll_views/material.dart';
4 6 import 'package:wow_english/common/core/user_util.dart';
5 7 import 'package:wow_english/common/extension/string_extension.dart';
6 8 import 'package:wow_english/models/course_unit_entity.dart';
  9 +import 'package:wow_english/pages/section/section_type.dart';
7 10 import 'package:wow_english/pages/section/widgets/home_video_item.dart';
8 11 import 'package:wow_english/pages/section/widgets/section_bouns_item.dart';
9 12 import 'package:wow_english/pages/section/widgets/section_header_widget.dart';
... ... @@ -16,17 +19,22 @@ import &#39;courese_module_model.dart&#39;;
16 19  
17 20 /// 环节列表页
18 21 class SectionPage extends StatelessWidget {
19   - const SectionPage({super.key, required this.courseUnitEntity, required this.courseUnitDetail});
  22 + const SectionPage(
  23 + {super.key, required this.courseUnitEntity, required this.courseUnitId});
20 24  
21 25 final CourseUnitEntity courseUnitEntity;
22 26  
23 27 /// unitId
24   - final CourseUnitDetail courseUnitDetail;
  28 + final int courseUnitId;
25 29  
26 30 @override
27 31 Widget build(BuildContext context) {
  32 + int initialPage = courseUnitEntity.courseUnitVOList
  33 + ?.indexWhere((element) => element.id == courseUnitId) ??
  34 + 0;
28 35 return BlocProvider(
29   - create: (context) => SectionBloc(courseUnitEntity, courseUnitDetail)..add(RequestDataEvent()),
  36 + create: (context) => SectionBloc(courseUnitEntity, initialPage,
  37 + PageController(initialPage: initialPage), ScrollController()),
30 38 child: _SectionPageView(context),
31 39 );
32 40 }
... ... @@ -45,15 +53,15 @@ class _SectionPageView extends StatelessWidget {
45 53 if (state is RequestVideoLessonState) {
46 54 final videoUrl = bloc.processEntity?.videos?.videoUrl ?? '';
47 55 var title = '';
48   - if (state.type == 1) {
  56 + if (state.type == SectionType.song.value) {
49 57 title = 'song';
50 58 }
51 59  
52   - if (state.type == 2) {
  60 + if (state.type == SectionType.video.value) {
53 61 title = 'video';
54 62 }
55 63  
56   - if (state.type == 5) {
  64 + if (state.type == SectionType.bouns.value) {
57 65 title = 'bonus';
58 66 }
59 67  
... ... @@ -63,22 +71,23 @@ class _SectionPageView extends StatelessWidget {
63 71 pushNamed(AppRouteName.lookVideo, arguments: {
64 72 'videoUrl': videoUrl,
65 73 'title': title,
66   - 'courseLessonId': state.courseLessonId
  74 + 'courseLessonId': state.courseLessonId,
  75 + 'isTopic': true
67 76 }).then((value) {
68 77 if (value != null) {
69   - Map<String, String> dataMap = value as Map<String, String>;
70   - bloc.add(RequestExitClassEvent(
71   - dataMap['courseLessonId']!,
72   - '0',
73   - dataMap['currentTime']!,
74   - ));
  78 + Map<String, dynamic> dataMap = value as Map<String, dynamic>;
  79 + bloc.add(RequestEndClassEvent(
  80 + dataMap['courseLessonId']!, dataMap['isCompleted'],
  81 + currentTime: dataMap['currentTime'],
  82 + autoNextSection: dataMap['nextSection']));
75 83 }
76 84 });
77 85 return;
78 86 }
79 87  
80 88 if (state is RequestEnterClassState) {
81   - if (state.courseType != 3 && state.courseType != 4) {
  89 + if (state.courseType != SectionType.practice.value &&
  90 + state.courseType != SectionType.pictureBook.value) {
82 91 ///视频类型
83 92 ///获取视频课程内容
84 93 bloc.add(RequestVideoLessonEvent(
... ... @@ -86,40 +95,46 @@ class _SectionPageView extends StatelessWidget {
86 95 return;
87 96 }
88 97  
89   - if (state.courseType == 4) {
  98 + if (state.courseType == SectionType.pictureBook.value) {
90 99 //绘本
91 100 pushNamed(AppRouteName.reading,
92 101 arguments: {'courseLessonId': state.courseLessonId})
93 102 .then((value) {
94 103 if (value != null) {
95   - Map<String, String> dataMap = value as Map<String, String>;
96   - bloc.add(RequestExitClassEvent(
97   - dataMap['courseLessonId']!, dataMap['currentStep']!, '0'));
  104 + Map<String, dynamic> dataMap = value as Map<String, dynamic>;
  105 + bloc.add(RequestEndClassEvent(
  106 + dataMap['courseLessonId']!,
  107 + dataMap['isCompleted'],
  108 + currentStep: dataMap['currentStep'],
  109 + autoNextSection: dataMap['nextSection'],
  110 + ));
98 111 }
99 112 });
100 113 return;
101 114 }
102 115  
103   - if (state.courseType == 3) {
  116 + if (state.courseType == SectionType.practice.value) {
104 117 //练习
105 118 pushNamed(AppRouteName.topicPic,
106 119 arguments: {'courseLessonId': state.courseLessonId})
107 120 .then((value) {
108 121 if (value != null) {
109   - Map<String, String> dataMap = value as Map<String, String>;
110   - bloc.add(RequestExitClassEvent(
111   - dataMap['courseLessonId']!, dataMap['currentStep']!, '0'));
  122 + Map<String, dynamic> dataMap = value as Map<String, dynamic>;
  123 + bloc.add(RequestEndClassEvent(
  124 + dataMap['courseLessonId']!, dataMap['isCompleted'],
  125 + currentStep: dataMap['currentStep'],
  126 + autoNextSection: dataMap['nextSection']));
112 127 }
113 128 });
114 129 return;
115 130 }
116 131 }
117 132 },
118   - child: _homeView(),
  133 + child: _sectionView(),
119 134 );
120 135 }
121 136  
122   - Widget _homeView() =>
  137 + Widget _sectionView() =>
123 138 BlocBuilder<SectionBloc, SectionState>(builder: (context, state) {
124 139 final bloc = BlocProvider.of<SectionBloc>(context);
125 140 return Scaffold(
... ... @@ -130,99 +145,71 @@ class _SectionPageView extends StatelessWidget {
130 145 mainAxisAlignment: MainAxisAlignment.spaceBetween,
131 146 children: [
132 147 SectionHeaderWidget(
133   - title: bloc.courseUnitDetail.name,
  148 + title: bloc.getCourseUnitDetail().name,
134 149 courseModuleCode: bloc.courseUnitEntity.courseModuleCode),
135 150 Expanded(
136   - child: ListView.builder(
137   - itemCount: bloc.courseSectionDatas?.length ?? 0,
138   - scrollDirection: Axis.horizontal,
139   - itemBuilder: (BuildContext context, int index) {
140   - CourseSectionEntity sectionData =
141   - bloc.courseSectionDatas![index];
142   - if (sectionData.courseType == 5) {
143   - //彩蛋
144   - return GestureDetector(
145   - onTap: () {
146   - if (!UserUtil.isLogined()) {
147   - pushNamed(AppRouteName.login);
148   - return;
149   - }
150   - if (sectionData.lock == true) {
151   - showToast('当前课程暂未解锁');
152   - return;
153   - }
154   -
155   - ///进入课堂
156   - bloc.add(RequestEnterClassEvent(
157   - sectionData.id.toString(), sectionData.courseType));
158   - },
159   - child: SectionBoundsItem(
160   - imageUrl: sectionData.coverUrl,
161   - ),
162   - );
163   - } else {
164   - return GestureDetector(
165   - onTap: () {
166   - if (!UserUtil.isLogined()) {
167   - pushNamed(AppRouteName.login);
168   - return;
169   - }
170   - if (sectionData.lock == true) {
171   - showToast('当前课程暂未解锁');
172   - return;
173   - }
174   -
175   - ///进入课堂
176   - bloc.add(RequestEnterClassEvent(
177   - sectionData.id.toString(), sectionData.courseType));
178   - },
179   - child: SectionVideoItem(
180   - unitEntity: bloc.courseUnitEntity,
181   - lessons: sectionData,
182   - ),
183   - );
184   - }
185   - })),
186   - // SafeArea(
187   - // child: Padding(
188   - // padding: EdgeInsets.symmetric(horizontal: 13.w),
189   - // child: Row(
190   - // mainAxisAlignment: MainAxisAlignment.spaceBetween,
191   - // children: [
192   - // SizedBox(
193   - // height: 47.h,
194   - // width: 80.w,
195   - // ),
196   - // Container(
197   - // decoration: BoxDecoration(
198   - // color: CourseModuleModel(
199   - // bloc.courseUnitEntity.courseModuleCode ??
200   - // 'Phase-1')
201   - // .color,
202   - // borderRadius: BorderRadius.circular(14.5.r),
203   - // ),
204   - // padding: EdgeInsets.symmetric(
205   - // vertical: 8.h, horizontal: 24.w),
206   - // child: Text(
207   - // '${(bloc.courseUnitEntity.nowStep ?? 0)}/${bloc.courseUnitEntity.total ?? 0}',
208   - // style: TextStyle(
209   - // color: Colors.white, fontSize: 12.sp),
210   - // ),
211   - // ),
212   - // Image.asset(
213   - // CourseModuleModel(
214   - // bloc.courseUnitEntity.courseModuleCode ??
215   - // 'Phase-1')
216   - // .courseModuleLogo
217   - // .assetPng,
218   - // height: 47.h,
219   - // width: 80.w,
220   - // // color: Colors.red,
221   - // ),
222   - // ],
223   - // ),
224   - // ),
225   - // )
  151 + child: Padding(
  152 + padding: EdgeInsets.symmetric(horizontal: 10.w),
  153 + child: OverflowBox(
  154 + child: NestedPageView.builder(
  155 + itemCount: bloc.unlockPageCount(),
  156 + controller: bloc.pageController,
  157 + onPageChanged: (int index) {
  158 + bloc.add(CurrentUnitIndexChangeEvent(index));
  159 + },
  160 + itemBuilder: (context, index) {
  161 + return ScrollConfiguration(
  162 + ///去掉 Android 上默认的边缘拖拽效果
  163 + behavior: ScrollConfiguration.of(context)
  164 + .copyWith(overscroll: false),
  165 + child: _itemTransCard(
  166 + bloc.getCourseUnitDetail(pageIndex: index),
  167 + index,
  168 + context),
  169 + );
  170 + }),
  171 + ), // 设置外部padding,
  172 + )),
  173 + SafeArea(
  174 + child: Padding(
  175 + padding: EdgeInsets.symmetric(horizontal: 13.w),
  176 + child: Row(
  177 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  178 + children: [
  179 + SizedBox(
  180 + height: 47.h,
  181 + width: 80.w,
  182 + ),
  183 + Container(
  184 + decoration: BoxDecoration(
  185 + color: CourseModuleModel(
  186 + bloc.courseUnitEntity.courseModuleCode ??
  187 + 'Phase-1')
  188 + .color,
  189 + borderRadius: BorderRadius.circular(14.5.r),
  190 + ),
  191 + padding: EdgeInsets.symmetric(
  192 + vertical: 8.h, horizontal: 24.w),
  193 + child: Text(
  194 + '${bloc.currentPage + 1}/${bloc.unlockPageCount()}',
  195 + style: TextStyle(
  196 + color: Colors.white, fontSize: 12.sp),
  197 + ),
  198 + ),
  199 + Image.asset(
  200 + CourseModuleModel(
  201 + bloc.courseUnitEntity.courseModuleCode ??
  202 + 'Phase-1')
  203 + .courseModuleLogo
  204 + .assetPng,
  205 + height: 47.h,
  206 + width: 80.w,
  207 + // color: Colors.red,
  208 + ),
  209 + ],
  210 + ),
  211 + ),
  212 + )
226 213 ],
227 214 ),
228 215 ),
... ... @@ -230,3 +217,81 @@ class _SectionPageView extends StatelessWidget {
230 217 );
231 218 });
232 219 }
  220 +
  221 +Widget _itemTransCard(
  222 + CourseUnitDetail courseUnitDetail, int pageIndex, BuildContext context) {
  223 + final bloc = BlocProvider.of<SectionBloc>(context);
  224 + List<CourseSectionEntity>? courseSectionEntities =
  225 + bloc.courseSectionDatasMap[courseUnitDetail.id];
  226 + if (courseSectionEntities == null) {
  227 + bloc.add(RequestDataEvent(courseUnitDetail.id!));
  228 + return Center(
  229 + child: Column(
  230 + mainAxisAlignment: MainAxisAlignment.center,
  231 + children: [
  232 + const Text(
  233 + '暂无数据',
  234 + style: TextStyle(fontSize: 24.0),
  235 + ),
  236 + const SizedBox(height: 16.0),
  237 + // 间距
  238 + CircularProgressIndicator(
  239 + color: CourseModuleModel(
  240 + bloc.courseUnitEntity.courseModuleCode ?? 'Phase-1')
  241 + .color),
  242 + // 加载动画
  243 + ],
  244 + ),
  245 + );
  246 + } else {
  247 + return NestedListView.builder(
  248 + itemCount: bloc.courseSectionDatasMap[courseUnitDetail.id]?.length ?? 0,
  249 + scrollDirection: Axis.horizontal,
  250 + itemBuilder: (BuildContext context, int index) {
  251 + CourseSectionEntity sectionData = courseSectionEntities[index];
  252 + if (sectionData.courseType == SectionType.bouns.value) {
  253 + //彩蛋
  254 + return GestureDetector(
  255 + onTap: () {
  256 + if (!UserUtil.isLogined()) {
  257 + pushNamed(AppRouteName.login);
  258 + return;
  259 + }
  260 + if (sectionData.lock == true) {
  261 + showToast('当前课程暂未解锁');
  262 + return;
  263 + }
  264 +
  265 + ///进入课堂
  266 + bloc.add(RequestEnterClassEvent(
  267 + sectionData.id.toString(), sectionData.courseType));
  268 + },
  269 + child: SectionBoundsItem(
  270 + imageUrl: sectionData.coverUrl,
  271 + ),
  272 + );
  273 + } else {
  274 + return GestureDetector(
  275 + onTap: () {
  276 + if (!UserUtil.isLogined()) {
  277 + pushNamed(AppRouteName.login);
  278 + return;
  279 + }
  280 + if (sectionData.lock == true) {
  281 + showToast('当前课程暂未解锁');
  282 + return;
  283 + }
  284 +
  285 + ///进入课堂
  286 + bloc.add(RequestEnterClassEvent(
  287 + sectionData.id.toString(), sectionData.courseType));
  288 + },
  289 + child: SectionVideoItem(
  290 + unitEntity: bloc.courseUnitEntity,
  291 + lessons: sectionData,
  292 + ),
  293 + );
  294 + }
  295 + });
  296 + }
  297 +}
... ...
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 sectionComplete(final VoidCallback? nextSectionTap,
  17 + {VoidCallback? againSectionTap, BuildContext? context}) {
  18 + // 逻辑来标记步骤为已完成
  19 + // 比如更新状态
  20 + if (isCompleteDialogShow) {
  21 + return;
  22 + }
  23 + isCompleteDialogShow = true;
  24 + showDialog(
  25 + context: context ?? AppRouter.context,
  26 + barrierDismissible: false,
  27 + barrierColor: Colors.black54,
  28 + builder: (BuildContext context) {
  29 + return AlertDialog(
  30 + backgroundColor: Colors.transparent,
  31 + content: SizedBox(
  32 + width: double.infinity, // 宽度设置为无限,使其尽可能铺满屏幕
  33 + height: MediaQuery.of(context).size.height * 0.6, // 高度设置为屏幕高度的60%
  34 + child: Row(
  35 + mainAxisAlignment: MainAxisAlignment.spaceBetween, // 图片之间分配空间
  36 + children: <Widget>[
  37 + Expanded(
  38 + flex: 1,
  39 + child: GestureDetector(
  40 + onTap: () {
  41 + popPage();
  42 + againSectionTap!();
  43 + },
  44 + child: Image.asset('section_finish_again'.assetPng),
  45 + ),
  46 + ),
  47 + Expanded(
  48 + flex: 2,
  49 + child: Image.asset('section_finish_steve'.assetPng),
  50 + ),
  51 + Expanded(
  52 + flex: 1,
  53 + child: GestureDetector(
  54 + onTap: () {
  55 + popPage();
  56 + nextSectionTap!();
  57 + },
  58 + child: Image.asset('section_finish_next'.assetPng),
  59 + ),
  60 + ),
  61 + ],
  62 + ),
  63 + ),
  64 + );
  65 + },
  66 + ).then((value) => isCompleteDialogShow = false);
  67 + }
  68 +}
... ...
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 +///环节完成事件
  4 +class SectionCompletedState extends BaseSectionState {}
  5 +
  6 +///环节再来一次事件
  7 +class SectionAgainState extends BaseSectionState {}
... ...
lib/pages/unit/view.dart
... ... @@ -64,7 +64,7 @@ class UnitPage extends StatelessWidget {
64 64 pushNamed(AppRouteName.courseSection,
65 65 arguments: {
66 66 'courseUnitEntity': bloc.unitData,
67   - 'courseUnitDetail': data
  67 + 'courseUnitId': data.id
68 68 });
69 69 },
70 70 child: CourseUnitItem(
... ...
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<BaseSectionEvent, BaseSectionState> {
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 });
  28 + on<SectionAgainEvent>((event, emit) {
  29 + emit(SectionAgainState());
  30 + });
16 31 }
17 32 }
... ...
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 {}
  5 +
  6 +class RestartVideoEvent extends LookVideoEvent {}
... ...
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/section/subsection/base_section/state.dart';
  4 +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
2 5 import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart';
3 6  
4   -class LookVideoPage extends StatefulWidget {
5   - const LookVideoPage({super.key, this.videoUrl, this.typeTitle, this.courseLessonId});
  7 +class LookVideoPage extends StatelessWidget {
  8 + const LookVideoPage(
  9 + {super.key, this.videoUrl, this.typeTitle, this.courseLessonId, this.isTopic = false});
6 10  
7 11 final String? videoUrl;
8 12 final String? typeTitle;
9 13 final String? courseLessonId;
  14 + final bool isTopic;
10 15  
11 16 @override
12   - State<StatefulWidget> createState() {
13   - return _LookVideoPageState();
14   - }
15   -}
16   -
17   -class _LookVideoPageState extends State<LookVideoPage> {
18   - @override
19 17 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   - ),
  18 + return BlocProvider(
  19 + create: (BuildContext context) => LookVideoBloc(videoUrl, typeTitle, courseLessonId, isTopic),
  20 + child: Builder(builder: (context) => _buildPage(context)),
27 21 );
28 22 }
29   -}
30 23 \ No newline at end of file
  24 +}
  25 +
  26 +Widget _buildPage(BuildContext context) {
  27 + return BlocBuilder<LookVideoBloc, BaseSectionState>(builder: (context, state) {
  28 + final bloc = BlocProvider.of<LookVideoBloc>(context);
  29 + return Container(
  30 + color: Colors.white,
  31 + child: VideoWidget(
  32 + videoUrl: bloc.videoUrl ?? '',
  33 + typeTitle: bloc.typeTitle ?? '',
  34 + courseLessonId: bloc.courseLessonId ?? '',
  35 + isTopic: bloc.isTopic,
  36 + )
  37 + );
  38 + }
  39 + );
  40 +}
... ...
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  
  10 +import '../../../section/subsection/base_section/event.dart';
  11 +import '../../../section/subsection/base_section/state.dart';
8 12 import 'video_opera_widget.dart';
9 13  
10 14 class VideoWidget extends StatefulWidget {
11   - const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = ''});
  15 + const VideoWidget({super.key,
  16 + this.videoUrl = '',
  17 + this.typeTitle,
  18 + this.courseLessonId = '',
  19 + this.isTopic = false});
12 20  
13 21 final String videoUrl;
14 22 final String? typeTitle;
15 23 final String courseLessonId;
  24 + final bool isTopic;
16 25  
17 26 @override
18 27 State<StatefulWidget> createState() {
... ... @@ -30,39 +39,62 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
30 39  
31 40 String formatDuration(Duration duration) {
32 41 String hours = duration.inHours.toString().padLeft(2, '0');
33   - String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
34   - String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
  42 + String minutes = duration.inMinutes.remainder(60).toString().padLeft(
  43 + 2, '0');
  44 + String seconds = duration.inSeconds.remainder(60).toString().padLeft(
  45 + 2, '0');
35 46 return "$hours:$minutes:$seconds";
36 47 }
37 48  
38 49 void _addListener() {
39 50 _controller!.addListener(() {
40   - if(_controller!.value.isInitialized) {
  51 + if (_controller!.value.isInitialized) {
41 52 if (_controller!.value.isPlaying) {
42 53 setState(() {
43   - double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble();
44   - int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60);
  54 + double currentSecond = getCurrentPositionSeconds().toDouble();
  55 + int totalSecond =
  56 + _controller!.value.duration.inMinutes.remainder(60) * 60 +
  57 + _controller!.value.duration.inSeconds.remainder(60);
45 58 _currentTime = formatDuration(_controller!.value.position);
46   - _playDegree = currentSecond/totalSecond;
  59 + _playDegree = currentSecond / totalSecond;
47 60 if (_playDegree > 1.0) {
48 61 _playDegree = 1.0;
49 62 }
50   - if(_playDegree < 0) {
  63 + if (_playDegree < 0) {
51 64 _playDegree = 0.0;
52 65 }
53 66 });
  67 + } else if (widget.isTopic &&
  68 + //受限于video_player库没有唯一的播放完成状态标识,发现isBuffering=false的时候暂时是安全唯一的
  69 + _controller?.value.isBuffering == false &&
  70 + _controller!.value.isCompleted &&
  71 + _controller!.value.position.inSeconds ==
  72 + _controller!.value.duration.inSeconds) {
  73 + final lookVideoBloc = context.read<LookVideoBloc>();
  74 + lookVideoBloc.sectionComplete(() {
  75 + popPage(data: {
  76 + 'courseLessonId': widget.courseLessonId,
  77 + 'currentTime': getCurrentPositionSeconds(),
  78 + 'isCompleted': true,
  79 + 'nextSection': widget.isTopic
  80 + });
  81 + } as VoidCallback,
  82 + againSectionTap: (() {
  83 + lookVideoBloc.add(SectionAgainEvent());
  84 + }), context: context);
54 85 }
55 86 }
56 87 });
57 88 }
58 89  
59 90 //开始倒计时
60   - void startTimer() {
61   - if(timerUtil == null) {
62   - timerUtil = TimerUtil(mInterval: 1000,mTotalTime: 1000*10);
  91 + void startTimer() {
  92 + if (timerUtil == null) {
  93 + timerUtil = TimerUtil(mInterval: 1000, mTotalTime: 1000 * 10);
63 94 timerUtil!.setOnTimerTickCallback((int tick) {
64 95 double currentTick = tick / 1000;
65   - if (currentTick.toInt() == 0) {//倒计时结束
  96 + if (currentTick.toInt() == 0) {
  97 + //倒计时结束
66 98 setState(() {
67 99 _hiddenTipView = true;
68 100 });
... ... @@ -89,8 +121,10 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
89 121 popPage();
90 122 return;
91 123 }
92   - String currentTime = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toString();
93   - popPage(data:{'courseLessonId':widget.courseLessonId,'currentTime':currentTime});
  124 + popPage(data: {
  125 + 'courseLessonId': widget.courseLessonId,
  126 + 'currentTime': getCurrentPositionSeconds(),
  127 + });
94 128 }
95 129 } else if (type == OperationType.playState) {
96 130 if (_controller!.value.isPlaying) {
... ... @@ -98,9 +132,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
98 132 } else {
99 133 _controller!.play();
100 134 }
101   - setState(() {
102   -
103   - });
  135 + setState(() {});
104 136 }
105 137 }
106 138  
... ... @@ -109,12 +141,12 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
109 141 super.initState();
110 142 Uri uri = Uri.parse(widget.videoUrl);
111 143 _controller = VideoPlayerController.networkUrl(uri)
112   - ..initialize().then((_){
  144 + ..initialize().then((_) {
113 145 startTimer();
114 146 setState(() {
115 147 _currentTime = formatDuration(_controller!.value.position);
116 148 _totalTime = formatDuration(_controller!.value.duration);
117   - _controller!.setLooping(true);
  149 + _controller!.setLooping(!widget.isTopic);
118 150 _controller!.setVolume(100);
119 151 _controller!.play();
120 152 });
... ... @@ -124,83 +156,95 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
124 156  
125 157 @override
126 158 Widget build(BuildContext context) {
127   - return GestureDetector(
128   - onTap: () {
129   - setState(() {
130   - _hiddenTipView = !_hiddenTipView;
131   - if(!_hiddenTipView) {
132   - startTimer();
133   - } else {
134   - if (timerUtil!.isActive()) {
135   - cancelTimer();
136   - }
137   - }
138   - });
  159 + return BlocListener<LookVideoBloc, BaseSectionState>(
  160 + listener: (context, state) async {
  161 + if (state is SectionAgainState) {
  162 + await _controller?.seekTo(Duration.zero);
  163 + _controller?.play();
  164 + }
139 165 },
140   - onDoubleTap: () {
141   - if(_controller!.value.isInitialized) {
142   - if (_controller!.value.isPlaying) {
143   - _controller!.pause();
144   - } else {
145   - _controller!.play();
146   - }
  166 + child: GestureDetector(
  167 + onTap: () {
147 168 setState(() {
148   -
  169 + _hiddenTipView = !_hiddenTipView;
  170 + if (!_hiddenTipView) {
  171 + startTimer();
  172 + } else {
  173 + if (timerUtil!.isActive()) {
  174 + cancelTimer();
  175 + }
  176 + }
149 177 });
150   - }
151   - },
152   - child: Center(
153   - child: _controller!.value.isInitialized ? Stack(
154   - alignment: Alignment.center,
155   - children: [
156   - SizedBox(
157   - height: double.infinity,
158   - width: double.infinity,
159   - child: AspectRatio(
160   - aspectRatio: _controller!.value.aspectRatio,
161   - child: VideoPlayer(_controller!),
162   - ),
163   - ),
164   - Offstage(
165   - offstage: _hiddenTipView,
166   - child: VideoOperaWidget(
167   - title: widget.typeTitle??'song',
168   - degree: _playDegree,
169   - totalTime: _totalTime,
170   - currentTime: _currentTime,
171   - isPlay: _controller!.value.isPlaying,
172   - actionEvent: (OperationType type) {
173   - actionType(type);
174   - },
175   - sliderChangeEvent: (double degree) {
176   - int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60);
177   - int positionSecond = (totalSecond * degree).toInt();
178   - _controller!.seekTo(Duration(seconds: positionSecond));
179   - },
  178 + },
  179 + onDoubleTap: () {
  180 + if (_controller!.value.isInitialized) {
  181 + if (_controller!.value.isPlaying) {
  182 + _controller!.pause();
  183 + } else {
  184 + _controller!.play();
  185 + }
  186 + setState(() {});
  187 + }
  188 + },
  189 + child: Center(
  190 + child: _controller!.value.isInitialized
  191 + ? Stack(
  192 + alignment: Alignment.center,
  193 + children: [
  194 + SizedBox(
  195 + height: double.infinity,
  196 + width: double.infinity,
  197 + child: AspectRatio(
  198 + aspectRatio: _controller!.value.aspectRatio,
  199 + child: VideoPlayer(_controller!),
  200 + ),
180 201 ),
181   - ),
182   - Offstage(
183   - offstage: _controller!.value.isPlaying,
184   - child: IconButton(
185   - onPressed: () {
186   - _controller!.play();
187   - },
188   - icon: Image.asset(
189   - 'video_stop'.assetPng,
190   - width: 70.w,
191   - height: 70.h,
  202 + Offstage(
  203 + offstage: _hiddenTipView,
  204 + child: VideoOperaWidget(
  205 + title: widget.typeTitle ?? 'song',
  206 + degree: _playDegree,
  207 + totalTime: _totalTime,
  208 + currentTime: _currentTime,
  209 + isPlay: _controller!.value.isPlaying,
  210 + actionEvent: (OperationType type) {
  211 + actionType(type);
  212 + },
  213 + sliderChangeEvent: (double degree) {
  214 + int totalSecond = _controller!
  215 + .value.duration.inMinutes
  216 + .remainder(60) *
  217 + 60 +
  218 + _controller!.value.duration.inSeconds
  219 + .remainder(60);
  220 + int positionSecond = (totalSecond * degree).toInt();
  221 + _controller!
  222 + .seekTo(Duration(seconds: positionSecond));
  223 + },
192 224 ),
193 225 ),
194   - )
195   - ],
196   - ): Container(
197   - color: Colors.white,
198   - child: Text(
199   - '视频加载中....',
200   - style: TextStyle(
201   - fontSize: 20.sp,
202   - color: Colors.black
203   - ),
  226 + Offstage(
  227 + offstage: _controller!.value.isPlaying,
  228 + child: IconButton(
  229 + onPressed: () {
  230 + _controller!.play();
  231 + },
  232 + icon: Image.asset(
  233 + 'video_stop'.assetPng,
  234 + width: 70.w,
  235 + height: 70.h,
  236 + ),
  237 + ),
  238 + )
  239 + ],
  240 + )
  241 + : Container(
  242 + color: Colors.white,
  243 + child: const CircularProgressIndicator(),
  244 + // Text(
  245 + // '视频加载中....',
  246 + // style: TextStyle(fontSize: 20.sp, color: Colors.black),
  247 + // ),
204 248 ),
205 249 ),
206 250 ),
... ... @@ -217,4 +261,10 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
217 261 }
218 262 super.dispose();
219 263 }
  264 +
  265 + ///获取当前进度秒数
  266 + int getCurrentPositionSeconds() {
  267 + return (_controller!.value.position.inMinutes.remainder(60) * 60 +
  268 + _controller!.value.position.inSeconds.remainder(60));
  269 + }
220 270 }
... ...
lib/route/route.dart
... ... @@ -126,17 +126,17 @@ class AppRouter {
126 126 builder: (_) => UnitPage(courseModuleEntity: courseModuleEntity));
127 127 case AppRouteName.courseSection:
128 128 CourseUnitEntity courseUnitEntity = CourseUnitEntity();
129   - CourseUnitDetail courseUnitDetail = CourseUnitDetail();
  129 + int courseUnitDetail = 0;
130 130 if (settings.arguments != null) {
131 131 courseUnitEntity = (settings.arguments as Map)
132 132 .getOrNull('courseUnitEntity') as CourseUnitEntity;
133 133 courseUnitDetail = (settings.arguments as Map)
134   - .getOrNull('courseUnitDetail') as CourseUnitDetail;
  134 + .getOrNull('courseUnitId');
135 135 }
136 136 return CupertinoPageRoute(
137 137 builder: (_) => SectionPage(
138 138 courseUnitEntity: courseUnitEntity,
139   - courseUnitDetail: courseUnitDetail));
  139 + courseUnitId: courseUnitDetail));
140 140 case AppRouteName.listen:
141 141 return CupertinoPageRoute(builder: (_) => const ListenPage());
142 142 case AppRouteName.shop:
... ... @@ -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
... ...
lib/utils/list_ext.dart 0 → 100644
  1 +
  2 +
  3 +extension ListExtension<E> on List<E> {
  4 + /// 获取数组中第一个匹配元素的index,没有就返回null
  5 + int? indexWhereOrNull(bool Function(E element) test) {
  6 + for (int i = 0; i < length; i++) {
  7 + if (test(this[i])) return i;
  8 + }
  9 + return null;
  10 + }
  11 +}
... ...
pubspec.yaml
... ... @@ -16,10 +16,11 @@ publish_to: &#39;none&#39; # Remove this line if you wish to publish to pub.dev
16 16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 17 # In Windows, build-name is used as the major, minor, and patch parts
18 18 # of the product and file versions while build-number is used as the build suffix.
19   -version: 1.0.3+3
  19 +version: 1.0.4+4
20 20  
21 21 environment:
22   - sdk: '>=3.0.0 <4.0.0'
  22 + sdk: '>=3.2.0 <4.0.0'
  23 + flutter: '>=3.16.0'
23 24  
24 25 # Dependencies specify other packages that your package needs in order to work.
25 26 # To automatically upgrade your package dependencies to the latest versions
... ... @@ -90,7 +91,7 @@ dependencies:
90 91 # 富文本插件 https://pub.dev/packages/extended_text
91 92 extended_text: ^11.0.1
92 93 # 视频播放 https://pub.dev/packages/video_player
93   - video_player: ^2.7.0
  94 + video_player: ^2.8.6
94 95 # UI适配 https://pub.dev/packages/responsive_framework
95 96 responsive_framework: ^1.0.0
96 97 # 音频播放 https://pub.dev/packages/audioplayers
... ... @@ -111,6 +112,8 @@ dependencies:
111 112 umeng_common_sdk: ^1.2.7
112 113 # 友盟APM https://pub-web.flutter-io.cn/packages/umeng_apm_sdk
113 114 umeng_apm_sdk: ^2.2.1
  115 + # 嵌套滚动 https://pub.dev/packages/nested_scroll_views
  116 + nested_scroll_views: ^0.0.10
114 117  
115 118 dev_dependencies:
116 119 build_runner: ^2.4.4
... ...