Commit 11f396024b5c792686719dc170d6153d58a0195f
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
lib/app/app.dart
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; | @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; | ||
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 'package:wow_english/pages/user/bloc/user_bloc.dart'; | @@ -10,7 +11,9 @@ import 'package:wow_english/pages/user/bloc/user_bloc.dart'; | ||
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 'package:wow_english/models/follow_read_entity.dart'; | @@ -4,6 +4,7 @@ import 'package:wow_english/models/follow_read_entity.dart'; | ||
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 'package:wow_english/common/extension/string_extension.dart'; | @@ -9,10 +9,14 @@ import 'package:wow_english/common/extension/string_extension.dart'; | ||
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<TopicPictureEvent, TopicPictureState> { | @@ -94,11 +98,15 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
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<TopicPictureEvent, TopicPictureState> { | @@ -263,6 +271,9 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
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<TopicPictureEvent, TopicPictureState> { | @@ -288,6 +299,7 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
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<TopicPictureEvent, TopicPictureState> { | @@ -302,4 +314,26 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
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 'package:wow_english/common/core/user_util.dart'; | @@ -6,6 +6,7 @@ import 'package:wow_english/common/core/user_util.dart'; | ||
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 'package:flutter_bloc/flutter_bloc.dart'; | @@ -6,11 +6,15 @@ import 'package:flutter_bloc/flutter_bloc.dart'; | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -211,7 +216,7 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -219,7 +224,7 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -265,8 +270,17 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -291,14 +305,14 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -313,7 +327,20 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -338,8 +365,11 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<ReadingPageEvent, ReadingPageState> { | @@ -354,11 +384,21 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
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<RepeatVideoWidget> { | @@ -140,13 +140,14 @@ class _RepeatVideoWidgetState extends State<RepeatVideoWidget> { | ||
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 'package:wow_english/utils/toast_util.dart'; | @@ -10,39 +10,55 @@ import 'package:wow_english/utils/toast_util.dart'; | ||
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<SectionEvent, SectionState> { | @@ -52,34 +68,156 @@ class SectionBloc extends Bloc<SectionEvent, SectionState> { | ||
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 'section_bloc.dart'; | @@ -3,12 +3,17 @@ part of 'section_bloc.dart'; | ||
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 'courese_module_model.dart'; | @@ -16,17 +19,22 @@ import 'courese_module_model.dart'; | ||
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
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
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<VideoWidget> { | @@ -30,39 +39,62 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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<VideoWidget> { | @@ -89,8 +121,10 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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<VideoWidget> { | @@ -98,9 +132,7 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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<VideoWidget> { | @@ -109,12 +141,12 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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<VideoWidget> { | @@ -124,83 +156,95 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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<VideoWidget> { | @@ -217,4 +261,10 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
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
pubspec.yaml
@@ -16,10 +16,11 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | @@ -16,10 +16,11 @@ publish_to: 'none' # 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 |