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,4 +24,22 @@
24 -keep class com.tt.** { *; } 24 -keep class com.tt.** { *; }
25 -keep class com.xs.** { *; } 25 -keep class com.xs.** { *; }
26 -keep interface com.xs.** { *; } 26 -keep interface com.xs.** { *; }
27 --keep enum com.xs.** { *; }  
28 \ No newline at end of file 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 \ No newline at end of file 47 \ No newline at end of file
android/app/src/main/AndroidManifest.xml
@@ -24,6 +24,8 @@ @@ -24,6 +24,8 @@
24 <intent-filter> 24 <intent-filter>
25 <action android:name="android.intent.action.MAIN"/> 25 <action android:name="android.intent.action.MAIN"/>
26 <category android:name="android.intent.category.LAUNCHER"/> 26 <category android:name="android.intent.category.LAUNCHER"/>
  27 + <!-- 友盟apm集成测试用 -->
  28 +<!-- <data android:scheme="um.663b66b0b3362515012f4ea5" />-->
27 </intent-filter> 29 </intent-filter>
28 </activity> 30 </activity>
29 <!-- Don't delete the meta-data below. 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,6 +12,8 @@ import com.kouyuxingqiu.wow_english.methodChannels.GameMethodChannel
12 import com.kouyuxingqiu.wow_english.methodChannels.SingSoungMethodChannel 12 import com.kouyuxingqiu.wow_english.methodChannels.SingSoungMethodChannel
13 import com.umeng.commonsdk.UMConfigure 13 import com.umeng.commonsdk.UMConfigure
14 import io.flutter.embedding.android.FlutterActivity 14 import io.flutter.embedding.android.FlutterActivity
  15 +import com.umeng.umcrash.UMCrash
  16 +
15 17
16 class MainActivity : FlutterActivity() { 18 class MainActivity : FlutterActivity() {
17 override fun onCreate(savedInstanceState: Bundle?) { 19 override fun onCreate(savedInstanceState: Bundle?) {
@@ -23,9 +25,7 @@ class MainActivity : FlutterActivity() { @@ -23,9 +25,7 @@ class MainActivity : FlutterActivity() {
23 SingSoungMethodChannel(this, it) 25 SingSoungMethodChannel(this, it)
24 GameMethodChannel(this, it) 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 override fun onResume() { 31 override fun onResume() {
@@ -56,4 +56,27 @@ class MainActivity : FlutterActivity() { @@ -56,4 +56,27 @@ class MainActivity : FlutterActivity() {
56 // 打开沉浸式 56 // 打开沉浸式
57 WindowCompat.setDecorFitsSystemWindows(window, false)*/ 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,6 +3,7 @@ import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;
3 import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 import 'package:flutter_easyloading/flutter_easyloading.dart';
4 import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
5 import 'package:responsive_framework/responsive_framework.dart'; 5 import 'package:responsive_framework/responsive_framework.dart';
  6 +import 'package:umeng_apm_sdk/umeng_apm_sdk.dart';
6 import 'package:wow_english/common/blocs/cachebloc/cache_bloc.dart'; 7 import 'package:wow_english/common/blocs/cachebloc/cache_bloc.dart';
7 import 'package:wow_english/common/widgets/hide_keyboard_widget.dart'; 8 import 'package:wow_english/common/widgets/hide_keyboard_widget.dart';
8 import 'package:wow_english/pages/tab/blocs/tab_bloc.dart'; 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,7 +11,9 @@ import &#39;package:wow_english/pages/user/bloc/user_bloc.dart&#39;;
10 import 'package:wow_english/route/route.dart'; 11 import 'package:wow_english/route/route.dart';
11 12
12 class App extends StatelessWidget { 13 class App extends StatelessWidget {
13 - const App({super.key}); 14 + const App([this._navigatorObserver]);
  15 +
  16 + final NavigatorObserver? _navigatorObserver;
14 17
15 @override 18 @override
16 Widget build(BuildContext context) { 19 Widget build(BuildContext context) {
@@ -41,6 +44,11 @@ class App extends StatelessWidget { @@ -41,6 +44,11 @@ class App extends StatelessWidget {
41 initialRoute: AppRouteName.splash, 44 initialRoute: AppRouteName.splash,
42 navigatorKey: AppRouter.navigatorKey, 45 navigatorKey: AppRouter.navigatorKey,
43 onGenerateRoute: AppRouter.generateRoute, 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,9 +89,12 @@ class Apis {
89 /// 进入课堂 89 /// 进入课堂
90 static const String enterClass = 'course/enter/class'; 90 static const String enterClass = 'course/enter/class';
91 91
92 - /// 退出课堂 92 + /// 退出课堂(非完整、中断)
93 static const String exitClass = 'course/exit/class'; 93 static const String exitClass = 'course/exit/class';
94 94
  95 + /// 结束课堂(完整)
  96 + static const String endClass = 'course/end/class';
  97 +
95 /// 商品列表 98 /// 商品列表
96 static const String productList = 'order/course/combo/list'; 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 class BasicConfig { 3 class BasicConfig {
4 // static bool isTestDev = true; 4 // static bool isTestDev = true;
@@ -7,7 +7,6 @@ class BasicConfig { @@ -7,7 +7,6 @@ class BasicConfig {
7 7
8 // 暂时未启用 8 // 暂时未启用
9 static bool isEnvProd() { 9 static bool isEnvProd() {
10 - bool kReleaseMode = const bool.fromEnvironment('dart.vm.product');  
11 if (kReleaseMode) { 10 if (kReleaseMode) {
12 debugPrint("dart.vm.product-现在是release环境."); 11 debugPrint("dart.vm.product-现在是release环境.");
13 } else { 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,6 +4,7 @@ import &#39;package:wow_english/models/follow_read_entity.dart&#39;;
4 import 'package:wow_english/models/listen_entity.dart'; 4 import 'package:wow_english/models/listen_entity.dart';
5 5
6 import '../../../models/read_content_entity.dart'; 6 import '../../../models/read_content_entity.dart';
  7 +import '../../../utils/date_util.dart';
7 8
8 class ListenDao { 9 class ListenDao {
9 /// 磨耳朵 10 /// 磨耳朵
@@ -14,37 +15,66 @@ class ListenDao { @@ -14,37 +15,66 @@ class ListenDao {
14 15
15 ///视频跟读 16 ///视频跟读
16 static Future<List<FollowReadEntity?>?> followRead() async { 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 return data; 20 return data;
19 } 21 }
20 22
21 ///课程内容 23 ///课程内容
22 static Future<CourseProcessEntity?> process(courseLessonId) async { 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 return data; 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 return data; 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 return data; 45 return data;
37 } 46 }
38 47
39 ///进入课堂 48 ///进入课堂
40 static Future enterClass(courseLessonId) async { 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 return data; 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 return data; 78 return data;
49 } 79 }
50 } 80 }
lib/main.dart
1 import 'dart:io'; 1 import 'dart:io';
2 2
  3 +import 'package:flutter/foundation.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart'; 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 import 'package:wow_english/app/app.dart'; 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 void main() { 21 void main() {
8 ///设置设备默认方向 22 ///设置设备默认方向
9 - WidgetsFlutterBinding.ensureInitialized();  
10 if (Platform.isAndroid) { 23 if (Platform.isAndroid) {
11 SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( //设置状态栏透明 24 SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( //设置状态栏透明
12 statusBarColor: Colors.transparent, 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,10 +9,14 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;;
9 import 'package:wow_english/common/request/dao/listen_dao.dart'; 9 import 'package:wow_english/common/request/dao/listen_dao.dart';
10 import 'package:wow_english/common/request/exception.dart'; 10 import 'package:wow_english/common/request/exception.dart';
11 import 'package:wow_english/models/course_process_entity.dart'; 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 import 'package:wow_english/utils/loading.dart'; 15 import 'package:wow_english/utils/loading.dart';
13 import 'package:wow_english/utils/toast_util.dart'; 16 import 'package:wow_english/utils/toast_util.dart';
14 17
15 import '../../../common/permission/permissionRequestPage.dart'; 18 import '../../../common/permission/permissionRequestPage.dart';
  19 +import '../../../route/route.dart';
16 20
17 part 'topic_picture_event.dart'; 21 part 'topic_picture_event.dart';
18 part 'topic_picture_state.dart'; 22 part 'topic_picture_state.dart';
@@ -28,7 +32,7 @@ enum VoicePlayState { @@ -28,7 +32,7 @@ enum VoicePlayState {
28 stop 32 stop
29 } 33 }
30 34
31 -class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { 35 +class TopicPictureBloc extends BaseSectionBloc<TopicPictureEvent, TopicPictureState> {
32 36
33 final PageController pageController; 37 final PageController pageController;
34 38
@@ -94,11 +98,15 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; { @@ -94,11 +98,15 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
94 _forbiddenWhenCorrect = false; 98 _forbiddenWhenCorrect = false;
95 debugPrint('播放完成后解除禁止'); 99 debugPrint('播放完成后解除禁止');
96 if (event == PlayerState.completed) { 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,6 +271,9 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
263 showToast('测评成功,分数是$overall',duration: const Duration(seconds: 5)); 271 showToast('测评成功,分数是$overall',duration: const Duration(seconds: 5));
264 _isVoicing = false; 272 _isVoicing = false;
265 emitter(XSVoiceTestState()); 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,6 +299,7 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
288 } 299 }
289 } 300 }
290 301
  302 + ///播放选择结果音效
291 void _playResultSound(bool isCorrect) async { 303 void _playResultSound(bool isCorrect) async {
292 // await audioPlayer.stop(); 304 // await audioPlayer.stop();
293 if (audioPlayer.state == PlayerState.playing && _isResultSoundPlaying == false) { 305 if (audioPlayer.state == PlayerState.playing && _isResultSoundPlaying == false) {
@@ -302,4 +314,26 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; { @@ -302,4 +314,26 @@ class TopicPictureBloc extends Bloc&lt;TopicPictureEvent, TopicPictureState&gt; {
302 await audioPlayer.play(AssetSource('incorrect_voice'.assetMp3)); 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 part of 'topic_picture_bloc.dart'; 1 part of 'topic_picture_bloc.dart';
2 2
3 @immutable 3 @immutable
4 -abstract class TopicPictureEvent {} 4 +abstract class TopicPictureEvent extends BaseSectionEvent {}
5 5
6 class InitBlocEvent extends TopicPictureEvent {} 6 class InitBlocEvent extends TopicPictureEvent {}
7 7
lib/pages/practice/bloc/topic_picture_state.dart
1 part of 'topic_picture_bloc.dart'; 1 part of 'topic_picture_bloc.dart';
2 2
3 @immutable 3 @immutable
4 -abstract class TopicPictureState {} 4 +abstract class TopicPictureState extends BaseSectionState {}
5 5
6 class TopicPictureInitial extends TopicPictureState {} 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 +6,7 @@ import &#39;package:wow_english/common/core/user_util.dart&#39;;
6 import 'package:wow_english/common/extension/string_extension.dart'; 6 import 'package:wow_english/common/extension/string_extension.dart';
7 import 'package:wow_english/common/widgets/ow_image_widget.dart'; 7 import 'package:wow_english/common/widgets/ow_image_widget.dart';
8 import 'package:wow_english/models/course_process_entity.dart'; 8 import 'package:wow_english/models/course_process_entity.dart';
  9 +import 'package:wow_english/pages/practice/topic_type.dart';
9 import 'package:wow_english/route/route.dart'; 10 import 'package:wow_english/route/route.dart';
10 import 'package:wow_english/utils/toast_util.dart'; 11 import 'package:wow_english/utils/toast_util.dart';
11 12
@@ -70,8 +71,9 @@ class _TopicPicturePage extends StatelessWidget { @@ -70,8 +71,9 @@ class _TopicPicturePage extends StatelessWidget {
70 onTap: () { 71 onTap: () {
71 popPage( 72 popPage(
72 data:{ 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 // Navigator.pop(context); 78 // Navigator.pop(context);
77 }, 79 },
@@ -86,13 +88,13 @@ class _TopicPicturePage extends StatelessWidget { @@ -86,13 +88,13 @@ class _TopicPicturePage extends StatelessWidget {
86 }, 88 },
87 itemBuilder: (BuildContext context,int index){ 89 itemBuilder: (BuildContext context,int index){
88 CourseProcessTopics? topics = bloc.entity?.topics![index]; 90 CourseProcessTopics? topics = bloc.entity?.topics![index];
89 - if (topics?.type == 1) {//听音选图 91 + if (topics?.type == TopicType.audioImageSelect.value) {//听音选图
90 return _pageViewVoicePictureItemWidget(topics); 92 return _pageViewVoicePictureItemWidget(topics);
91 - } else if (topics?.type == 2) {//听音选字 93 + } else if (topics?.type == TopicType.audioCharSelect.value) {//听音选字
92 return _pageViewVoiceWordItemWidget(topics); 94 return _pageViewVoiceWordItemWidget(topics);
93 - } else if (topics?.type == 3) {//看题选字 95 + } else if (topics?.type == TopicType.questionCharSelect.value) {//看题选字
94 return _pageViewWordItemWidget(topics); 96 return _pageViewWordItemWidget(topics);
95 - } else if (topics?.type == 4) {//看题选图 97 + } else if (topics?.type == TopicType.questionImageSelect.value) {//看题选图
96 return _pageViewItemWidget(topics); 98 return _pageViewItemWidget(topics);
97 } else {//语音问答 99 } else {//语音问答
98 return _voiceAnswerItem(topics); 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,11 +6,15 @@ import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;
6 import 'package:flutter_easyloading/flutter_easyloading.dart'; 6 import 'package:flutter_easyloading/flutter_easyloading.dart';
7 import 'package:permission_handler/permission_handler.dart'; 7 import 'package:permission_handler/permission_handler.dart';
8 import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; 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 import '../../../common/core/user_util.dart'; 13 import '../../../common/core/user_util.dart';
11 import '../../../common/request/dao/listen_dao.dart'; 14 import '../../../common/request/dao/listen_dao.dart';
12 import '../../../common/request/exception.dart'; 15 import '../../../common/request/exception.dart';
13 import '../../../models/course_process_entity.dart'; 16 import '../../../models/course_process_entity.dart';
  17 +import '../../../route/route.dart';
14 import '../../../utils/loading.dart'; 18 import '../../../utils/loading.dart';
15 19
16 import '../../../utils/log_util.dart'; 20 import '../../../utils/log_util.dart';
@@ -33,7 +37,8 @@ enum VoicePlayState { @@ -33,7 +37,8 @@ enum VoicePlayState {
33 stop 37 stop
34 } 38 }
35 39
36 -class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { 40 +class ReadingPageBloc
  41 + extends BaseSectionBloc<ReadingPageEvent, ReadingPageState> {
37 final PageController pageController; 42 final PageController pageController;
38 43
39 final String courseLessonId; 44 final String courseLessonId;
@@ -211,7 +216,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -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 if (_isRecordAudioPlaying) { 220 if (_isRecordAudioPlaying) {
216 _isRecordAudioPlaying = false; 221 _isRecordAudioPlaying = false;
217 } 222 }
@@ -219,7 +224,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -219,7 +224,7 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
219 "_playOriginalAudio _isRecordAudioPlaying=$_isRecordAudioPlaying _isOriginAudioPlaying=$_isOriginAudioPlaying url=$audioUrl"); 224 "_playOriginalAudio _isRecordAudioPlaying=$_isRecordAudioPlaying _isOriginAudioPlaying=$_isOriginAudioPlaying url=$audioUrl");
220 if (_isOriginAudioPlaying) { 225 if (_isOriginAudioPlaying) {
221 _isOriginAudioPlaying = false; 226 _isOriginAudioPlaying = false;
222 - await audioPlayer.stop(); 227 + audioPlayer.stop();
223 } else { 228 } else {
224 _isOriginAudioPlaying = true; 229 _isOriginAudioPlaying = true;
225 audioUrl ??= currentPageData()?.audioUrl ?? ''; 230 audioUrl ??= currentPageData()?.audioUrl ?? '';
@@ -265,8 +270,17 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -265,8 +270,17 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
265 } 270 }
266 271
267 void nextPage() { 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 } else { 284 } else {
271 _currentPage += 1; 285 _currentPage += 1;
272 pageController.nextPage( 286 pageController.nextPage(
@@ -291,14 +305,14 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -291,14 +305,14 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
291 305
292 void startRecord(String content) async { 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 if (result) { 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,7 +327,20 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
313 currentPageData()?.recordScore = overall; 327 currentPageData()?.recordScore = overall;
314 currentPageData()?.recordUrl = args['audioUrl'] + '.mp3'; 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 // emitter(FeedbackState()); 344 // emitter(FeedbackState());
318 } 345 }
319 346
@@ -338,8 +365,11 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -338,8 +365,11 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
338 nextPage(); 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 startRecord(currentPageData()?.word ?? ''); 374 startRecord(currentPageData()?.word ?? '');
345 } 375 }
@@ -354,11 +384,21 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; { @@ -354,11 +384,21 @@ class ReadingPageBloc extends Bloc&lt;ReadingPageEvent, ReadingPageState&gt; {
354 _isRecordAudioPlaying = false; 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 emitter(XSVoiceTestState()); 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 part of 'reading_bloc.dart'; 1 part of 'reading_bloc.dart';
2 2
3 @immutable 3 @immutable
4 -abstract class ReadingPageEvent {} 4 +abstract class ReadingPageEvent extends BaseSectionEvent {}
5 5
6 ///页面初始化 6 ///页面初始化
7 class InitBlocEvent extends ReadingPageEvent {} 7 class InitBlocEvent extends ReadingPageEvent {}
lib/pages/reading/bloc/reading_state.dart
1 part of 'reading_bloc.dart'; 1 part of 'reading_bloc.dart';
2 2
3 @immutable 3 @immutable
4 -abstract class ReadingPageState {} 4 +abstract class ReadingPageState extends BaseSectionState {}
5 5
6 class ReadingPageInitial extends ReadingPageState {} 6 class ReadingPageInitial extends ReadingPageState {}
7 7
lib/pages/reading/reading_page.dart
@@ -88,8 +88,9 @@ class _ReadingPage extends StatelessWidget { @@ -88,8 +88,9 @@ class _ReadingPage extends StatelessWidget {
88 onPressed: () { 88 onPressed: () {
89 popPage( 89 popPage(
90 data:{ 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,13 +140,14 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
140 : 140 :
141 Container( 141 Container(
142 color: Colors.white, 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,39 +10,55 @@ import &#39;package:wow_english/utils/toast_util.dart&#39;;
10 10
11 import '../../../models/course_section_entity.dart'; 11 import '../../../models/course_section_entity.dart';
12 import '../../../models/course_unit_entity.dart'; 12 import '../../../models/course_unit_entity.dart';
  13 +import '../../../utils/list_ext.dart';
13 14
14 part 'section_event.dart'; 15 part 'section_event.dart';
15 part 'section_state.dart'; 16 part 'section_state.dart';
16 17
17 class SectionBloc extends Bloc<SectionEvent, SectionState> { 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 CourseProcessEntity? _processEntity; 42 CourseProcessEntity? _processEntity;
32 43
33 CourseProcessEntity? get processEntity => _processEntity; 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 on<RequestEnterClassEvent>(_requestEnterClass); 51 on<RequestEnterClassEvent>(_requestEnterClass);
39 on<RequestVideoLessonEvent>(_requestVideoLesson); 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 try { 58 try {
44 await loading(() async { 59 await loading(() async {
45 - _courseSectionDatas = await LessonDao.courseSection(courseUnitId: _courseUnitDetail.id!); 60 + _courseSectionDatasMap[event.courseUnitId] =
  61 + await LessonDao.courseSection(courseUnitId: event.courseUnitId);
46 emitter(LessonDataLoadState()); 62 emitter(LessonDataLoadState());
47 }); 63 });
48 } catch (e) { 64 } catch (e) {
@@ -52,34 +68,156 @@ class SectionBloc extends Bloc&lt;SectionEvent, SectionState&gt; { @@ -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 try { 73 try {
57 await loading(() async { 74 await loading(() async {
58 _processEntity = await ListenDao.process(event.courseLessonId); 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 } catch (e) { 79 } catch (e) {
62 if (e is ApiException) { 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 try { 88 try {
71 await loading(() async { 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 } catch (e) { 93 } catch (e) {
76 if (e is ApiException) { 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,12 +3,17 @@ part of &#39;section_bloc.dart&#39;;
3 @immutable 3 @immutable
4 abstract class SectionEvent {} 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 class RequestVideoLessonEvent extends SectionEvent { 13 class RequestVideoLessonEvent extends SectionEvent {
10 final String courseLessonId; 14 final String courseLessonId;
11 final int courseType; 15 final int courseType;
  16 +
12 RequestVideoLessonEvent(this.courseLessonId, this.courseType); 17 RequestVideoLessonEvent(this.courseLessonId, this.courseType);
13 } 18 }
14 19
@@ -16,13 +21,35 @@ class RequestVideoLessonEvent extends SectionEvent { @@ -16,13 +21,35 @@ class RequestVideoLessonEvent extends SectionEvent {
16 class RequestEnterClassEvent extends SectionEvent { 21 class RequestEnterClassEvent extends SectionEvent {
17 final String courseLessonId; 22 final String courseLessonId;
18 final int courseType; 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 final String courseLessonId; 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,11 +10,15 @@ class LessonDataLoadState extends SectionState {}
10 class RequestVideoLessonState extends SectionState { 10 class RequestVideoLessonState extends SectionState {
11 final String courseLessonId; 11 final String courseLessonId;
12 final int type; 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 final String courseLessonId; 18 final String courseLessonId;
18 final int courseType; 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 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 import 'package:flutter_bloc/flutter_bloc.dart';
3 import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
  5 +import 'package:nested_scroll_views/material.dart';
4 import 'package:wow_english/common/core/user_util.dart'; 6 import 'package:wow_english/common/core/user_util.dart';
5 import 'package:wow_english/common/extension/string_extension.dart'; 7 import 'package:wow_english/common/extension/string_extension.dart';
6 import 'package:wow_english/models/course_unit_entity.dart'; 8 import 'package:wow_english/models/course_unit_entity.dart';
  9 +import 'package:wow_english/pages/section/section_type.dart';
7 import 'package:wow_english/pages/section/widgets/home_video_item.dart'; 10 import 'package:wow_english/pages/section/widgets/home_video_item.dart';
8 import 'package:wow_english/pages/section/widgets/section_bouns_item.dart'; 11 import 'package:wow_english/pages/section/widgets/section_bouns_item.dart';
9 import 'package:wow_english/pages/section/widgets/section_header_widget.dart'; 12 import 'package:wow_english/pages/section/widgets/section_header_widget.dart';
@@ -16,17 +19,22 @@ import &#39;courese_module_model.dart&#39;; @@ -16,17 +19,22 @@ import &#39;courese_module_model.dart&#39;;
16 19
17 /// 环节列表页 20 /// 环节列表页
18 class SectionPage extends StatelessWidget { 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 final CourseUnitEntity courseUnitEntity; 25 final CourseUnitEntity courseUnitEntity;
22 26
23 /// unitId 27 /// unitId
24 - final CourseUnitDetail courseUnitDetail; 28 + final int courseUnitId;
25 29
26 @override 30 @override
27 Widget build(BuildContext context) { 31 Widget build(BuildContext context) {
  32 + int initialPage = courseUnitEntity.courseUnitVOList
  33 + ?.indexWhere((element) => element.id == courseUnitId) ??
  34 + 0;
28 return BlocProvider( 35 return BlocProvider(
29 - create: (context) => SectionBloc(courseUnitEntity, courseUnitDetail)..add(RequestDataEvent()), 36 + create: (context) => SectionBloc(courseUnitEntity, initialPage,
  37 + PageController(initialPage: initialPage), ScrollController()),
30 child: _SectionPageView(context), 38 child: _SectionPageView(context),
31 ); 39 );
32 } 40 }
@@ -45,15 +53,15 @@ class _SectionPageView extends StatelessWidget { @@ -45,15 +53,15 @@ class _SectionPageView extends StatelessWidget {
45 if (state is RequestVideoLessonState) { 53 if (state is RequestVideoLessonState) {
46 final videoUrl = bloc.processEntity?.videos?.videoUrl ?? ''; 54 final videoUrl = bloc.processEntity?.videos?.videoUrl ?? '';
47 var title = ''; 55 var title = '';
48 - if (state.type == 1) { 56 + if (state.type == SectionType.song.value) {
49 title = 'song'; 57 title = 'song';
50 } 58 }
51 59
52 - if (state.type == 2) { 60 + if (state.type == SectionType.video.value) {
53 title = 'video'; 61 title = 'video';
54 } 62 }
55 63
56 - if (state.type == 5) { 64 + if (state.type == SectionType.bouns.value) {
57 title = 'bonus'; 65 title = 'bonus';
58 } 66 }
59 67
@@ -63,22 +71,23 @@ class _SectionPageView extends StatelessWidget { @@ -63,22 +71,23 @@ class _SectionPageView extends StatelessWidget {
63 pushNamed(AppRouteName.lookVideo, arguments: { 71 pushNamed(AppRouteName.lookVideo, arguments: {
64 'videoUrl': videoUrl, 72 'videoUrl': videoUrl,
65 'title': title, 73 'title': title,
66 - 'courseLessonId': state.courseLessonId 74 + 'courseLessonId': state.courseLessonId,
  75 + 'isTopic': true
67 }).then((value) { 76 }).then((value) {
68 if (value != null) { 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 return; 85 return;
78 } 86 }
79 87
80 if (state is RequestEnterClassState) { 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 bloc.add(RequestVideoLessonEvent( 93 bloc.add(RequestVideoLessonEvent(
@@ -86,40 +95,46 @@ class _SectionPageView extends StatelessWidget { @@ -86,40 +95,46 @@ class _SectionPageView extends StatelessWidget {
86 return; 95 return;
87 } 96 }
88 97
89 - if (state.courseType == 4) { 98 + if (state.courseType == SectionType.pictureBook.value) {
90 //绘本 99 //绘本
91 pushNamed(AppRouteName.reading, 100 pushNamed(AppRouteName.reading,
92 arguments: {'courseLessonId': state.courseLessonId}) 101 arguments: {'courseLessonId': state.courseLessonId})
93 .then((value) { 102 .then((value) {
94 if (value != null) { 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 return; 113 return;
101 } 114 }
102 115
103 - if (state.courseType == 3) { 116 + if (state.courseType == SectionType.practice.value) {
104 //练习 117 //练习
105 pushNamed(AppRouteName.topicPic, 118 pushNamed(AppRouteName.topicPic,
106 arguments: {'courseLessonId': state.courseLessonId}) 119 arguments: {'courseLessonId': state.courseLessonId})
107 .then((value) { 120 .then((value) {
108 if (value != null) { 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 return; 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 BlocBuilder<SectionBloc, SectionState>(builder: (context, state) { 138 BlocBuilder<SectionBloc, SectionState>(builder: (context, state) {
124 final bloc = BlocProvider.of<SectionBloc>(context); 139 final bloc = BlocProvider.of<SectionBloc>(context);
125 return Scaffold( 140 return Scaffold(
@@ -130,99 +145,71 @@ class _SectionPageView extends StatelessWidget { @@ -130,99 +145,71 @@ class _SectionPageView extends StatelessWidget {
130 mainAxisAlignment: MainAxisAlignment.spaceBetween, 145 mainAxisAlignment: MainAxisAlignment.spaceBetween,
131 children: [ 146 children: [
132 SectionHeaderWidget( 147 SectionHeaderWidget(
133 - title: bloc.courseUnitDetail.name, 148 + title: bloc.getCourseUnitDetail().name,
134 courseModuleCode: bloc.courseUnitEntity.courseModuleCode), 149 courseModuleCode: bloc.courseUnitEntity.courseModuleCode),
135 Expanded( 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,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 \ No newline at end of file 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,7 +64,7 @@ class UnitPage extends StatelessWidget {
64 pushNamed(AppRouteName.courseSection, 64 pushNamed(AppRouteName.courseSection,
65 arguments: { 65 arguments: {
66 'courseUnitEntity': bloc.unitData, 66 'courseUnitEntity': bloc.unitData,
67 - 'courseUnitDetail': data 67 + 'courseUnitId': data.id
68 }); 68 });
69 }, 69 },
70 child: CourseUnitItem( 70 child: CourseUnitItem(
lib/pages/video/lookvideo/bloc/look_video_bloc.dart
1 import 'package:flutter/cupertino.dart'; 1 import 'package:flutter/cupertino.dart';
2 import 'package:flutter_bloc/flutter_bloc.dart'; 2 import 'package:flutter_bloc/flutter_bloc.dart';
3 import 'package:video_player/video_player.dart'; 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 part 'look_video_event.dart'; 8 part 'look_video_event.dart';
6 part 'look_video_state.dart'; 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 VideoPlayerController? _controller; 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 on<LookVideoEvent>((event, emit) { 25 on<LookVideoEvent>((event, emit) {
14 // TODO: implement event handler 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 part of 'look_video_bloc.dart'; 1 part of 'look_video_bloc.dart';
2 2
3 @immutable 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 part of 'look_video_bloc.dart'; 1 part of 'look_video_bloc.dart';
2 2
3 @immutable 3 @immutable
4 -abstract class LookVideoState {} 4 +abstract class LookVideoState extends BaseSectionState {}
5 5
6 class LookVideoInitial extends LookVideoState {} 6 class LookVideoInitial extends LookVideoState {}
7 7
lib/pages/video/lookvideo/look_video_page.dart
1 import 'package:flutter/material.dart'; 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 import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart'; 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 final String? videoUrl; 11 final String? videoUrl;
8 final String? typeTitle; 12 final String? typeTitle;
9 final String? courseLessonId; 13 final String? courseLessonId;
  14 + final bool isTopic;
10 15
11 @override 16 @override
12 - State<StatefulWidget> createState() {  
13 - return _LookVideoPageState();  
14 - }  
15 -}  
16 -  
17 -class _LookVideoPageState extends State<LookVideoPage> {  
18 - @override  
19 Widget build(BuildContext context) { 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 \ No newline at end of file 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 import 'package:common_utils/common_utils.dart'; 1 import 'package:common_utils/common_utils.dart';
2 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
  3 +import 'package:flutter_bloc/flutter_bloc.dart';
3 import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 import 'package:video_player/video_player.dart'; 5 import 'package:video_player/video_player.dart';
5 import 'package:wow_english/common/extension/string_extension.dart'; 6 import 'package:wow_english/common/extension/string_extension.dart';
  7 +import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
6 import 'package:wow_english/route/route.dart'; 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 import 'video_opera_widget.dart'; 12 import 'video_opera_widget.dart';
9 13
10 class VideoWidget extends StatefulWidget { 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 final String videoUrl; 21 final String videoUrl;
14 final String? typeTitle; 22 final String? typeTitle;
15 final String courseLessonId; 23 final String courseLessonId;
  24 + final bool isTopic;
16 25
17 @override 26 @override
18 State<StatefulWidget> createState() { 27 State<StatefulWidget> createState() {
@@ -30,39 +39,62 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -30,39 +39,62 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
30 39
31 String formatDuration(Duration duration) { 40 String formatDuration(Duration duration) {
32 String hours = duration.inHours.toString().padLeft(2, '0'); 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 return "$hours:$minutes:$seconds"; 46 return "$hours:$minutes:$seconds";
36 } 47 }
37 48
38 void _addListener() { 49 void _addListener() {
39 _controller!.addListener(() { 50 _controller!.addListener(() {
40 - if(_controller!.value.isInitialized) { 51 + if (_controller!.value.isInitialized) {
41 if (_controller!.value.isPlaying) { 52 if (_controller!.value.isPlaying) {
42 setState(() { 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 _currentTime = formatDuration(_controller!.value.position); 58 _currentTime = formatDuration(_controller!.value.position);
46 - _playDegree = currentSecond/totalSecond; 59 + _playDegree = currentSecond / totalSecond;
47 if (_playDegree > 1.0) { 60 if (_playDegree > 1.0) {
48 _playDegree = 1.0; 61 _playDegree = 1.0;
49 } 62 }
50 - if(_playDegree < 0) { 63 + if (_playDegree < 0) {
51 _playDegree = 0.0; 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 timerUtil!.setOnTimerTickCallback((int tick) { 94 timerUtil!.setOnTimerTickCallback((int tick) {
64 double currentTick = tick / 1000; 95 double currentTick = tick / 1000;
65 - if (currentTick.toInt() == 0) {//倒计时结束 96 + if (currentTick.toInt() == 0) {
  97 + //倒计时结束
66 setState(() { 98 setState(() {
67 _hiddenTipView = true; 99 _hiddenTipView = true;
68 }); 100 });
@@ -89,8 +121,10 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -89,8 +121,10 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
89 popPage(); 121 popPage();
90 return; 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 } else if (type == OperationType.playState) { 129 } else if (type == OperationType.playState) {
96 if (_controller!.value.isPlaying) { 130 if (_controller!.value.isPlaying) {
@@ -98,9 +132,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -98,9 +132,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
98 } else { 132 } else {
99 _controller!.play(); 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,12 +141,12 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
109 super.initState(); 141 super.initState();
110 Uri uri = Uri.parse(widget.videoUrl); 142 Uri uri = Uri.parse(widget.videoUrl);
111 _controller = VideoPlayerController.networkUrl(uri) 143 _controller = VideoPlayerController.networkUrl(uri)
112 - ..initialize().then((_){ 144 + ..initialize().then((_) {
113 startTimer(); 145 startTimer();
114 setState(() { 146 setState(() {
115 _currentTime = formatDuration(_controller!.value.position); 147 _currentTime = formatDuration(_controller!.value.position);
116 _totalTime = formatDuration(_controller!.value.duration); 148 _totalTime = formatDuration(_controller!.value.duration);
117 - _controller!.setLooping(true); 149 + _controller!.setLooping(!widget.isTopic);
118 _controller!.setVolume(100); 150 _controller!.setVolume(100);
119 _controller!.play(); 151 _controller!.play();
120 }); 152 });
@@ -124,83 +156,95 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -124,83 +156,95 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
124 156
125 @override 157 @override
126 Widget build(BuildContext context) { 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 setState(() { 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,4 +261,10 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
217 } 261 }
218 super.dispose(); 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,17 +126,17 @@ class AppRouter {
126 builder: (_) => UnitPage(courseModuleEntity: courseModuleEntity)); 126 builder: (_) => UnitPage(courseModuleEntity: courseModuleEntity));
127 case AppRouteName.courseSection: 127 case AppRouteName.courseSection:
128 CourseUnitEntity courseUnitEntity = CourseUnitEntity(); 128 CourseUnitEntity courseUnitEntity = CourseUnitEntity();
129 - CourseUnitDetail courseUnitDetail = CourseUnitDetail(); 129 + int courseUnitDetail = 0;
130 if (settings.arguments != null) { 130 if (settings.arguments != null) {
131 courseUnitEntity = (settings.arguments as Map) 131 courseUnitEntity = (settings.arguments as Map)
132 .getOrNull('courseUnitEntity') as CourseUnitEntity; 132 .getOrNull('courseUnitEntity') as CourseUnitEntity;
133 courseUnitDetail = (settings.arguments as Map) 133 courseUnitDetail = (settings.arguments as Map)
134 - .getOrNull('courseUnitDetail') as CourseUnitDetail; 134 + .getOrNull('courseUnitId');
135 } 135 }
136 return CupertinoPageRoute( 136 return CupertinoPageRoute(
137 builder: (_) => SectionPage( 137 builder: (_) => SectionPage(
138 courseUnitEntity: courseUnitEntity, 138 courseUnitEntity: courseUnitEntity,
139 - courseUnitDetail: courseUnitDetail)); 139 + courseUnitId: courseUnitDetail));
140 case AppRouteName.listen: 140 case AppRouteName.listen:
141 return CupertinoPageRoute(builder: (_) => const ListenPage()); 141 return CupertinoPageRoute(builder: (_) => const ListenPage());
142 case AppRouteName.shop: 142 case AppRouteName.shop:
@@ -190,11 +190,14 @@ class AppRouter { @@ -190,11 +190,14 @@ class AppRouter {
190 final title = (settings.arguments as Map)['title'] as String?; 190 final title = (settings.arguments as Map)['title'] as String?;
191 final courseLessonId = 191 final courseLessonId =
192 (settings.arguments as Map)['courseLessonId'] as String?; 192 (settings.arguments as Map)['courseLessonId'] as String?;
  193 + ///是否是课程内的视频环节,用于播放结束判断要不要再来一次以及下一环节用
  194 + final isTopic = (settings.arguments as Map)['isTopic'] as bool? ?? false;
193 return CupertinoPageRoute( 195 return CupertinoPageRoute(
194 builder: (_) => LookVideoPage( 196 builder: (_) => LookVideoPage(
195 videoUrl: videoUrl, 197 videoUrl: videoUrl,
196 typeTitle: title, 198 typeTitle: title,
197 courseLessonId: courseLessonId, 199 courseLessonId: courseLessonId,
  200 + isTopic: isTopic,
198 )); 201 ));
199 /*case AppRouteName.setPwd: 202 /*case AppRouteName.setPwd:
200 case AppRouteName.setPwd: 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 \ No newline at end of file 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,10 +16,11 @@ publish_to: &#39;none&#39; # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts 17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix. 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 environment: 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 # Dependencies specify other packages that your package needs in order to work. 25 # Dependencies specify other packages that your package needs in order to work.
25 # To automatically upgrade your package dependencies to the latest versions 26 # To automatically upgrade your package dependencies to the latest versions
@@ -90,7 +91,7 @@ dependencies: @@ -90,7 +91,7 @@ dependencies:
90 # 富文本插件 https://pub.dev/packages/extended_text 91 # 富文本插件 https://pub.dev/packages/extended_text
91 extended_text: ^11.0.1 92 extended_text: ^11.0.1
92 # 视频播放 https://pub.dev/packages/video_player 93 # 视频播放 https://pub.dev/packages/video_player
93 - video_player: ^2.7.0 94 + video_player: ^2.8.6
94 # UI适配 https://pub.dev/packages/responsive_framework 95 # UI适配 https://pub.dev/packages/responsive_framework
95 responsive_framework: ^1.0.0 96 responsive_framework: ^1.0.0
96 # 音频播放 https://pub.dev/packages/audioplayers 97 # 音频播放 https://pub.dev/packages/audioplayers
@@ -111,6 +112,8 @@ dependencies: @@ -111,6 +112,8 @@ dependencies:
111 umeng_common_sdk: ^1.2.7 112 umeng_common_sdk: ^1.2.7
112 # 友盟APM https://pub-web.flutter-io.cn/packages/umeng_apm_sdk 113 # 友盟APM https://pub-web.flutter-io.cn/packages/umeng_apm_sdk
113 umeng_apm_sdk: ^2.2.1 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 dev_dependencies: 118 dev_dependencies:
116 build_runner: ^2.4.4 119 build_runner: ^2.4.4