diff --git a/.gitignore b/.gitignore index 556e69a..702f171 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ **/ios/xcode_build_ipa_adh **/ios/xcode_build_ipa_aps **/ios/xcode_build_ipa_dev +**/ios/build/ # Exceptions to above rules. !**/ios/**/default.mode1v3 diff --git a/lib/pages/home/bloc.dart b/lib/pages/home/bloc.dart index 325ea27..7d455df 100644 --- a/lib/pages/home/bloc.dart +++ b/lib/pages/home/bloc.dart @@ -16,7 +16,6 @@ class HomeBloc extends Bloc { void _init(InitEvent event, Emitter emit) async { await _checkUpdate(emit); - debugPrint('WQF ModuleSelectBloc _init'); } Future _checkUpdate(Emitter emit) async { diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 7f9b843..12632b3 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -13,7 +13,6 @@ import 'package:wow_english/pages/user/bloc/user_bloc.dart'; import '../../common/core/user_util.dart'; import '../../common/dialogs/show_dialog.dart'; -import '../../utils/log_util.dart'; import 'bloc.dart'; import 'event.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -38,11 +37,9 @@ class _HomePageView extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocListener(listeners: [ BlocListener(listener: (context, state) { - debugPrint('WQF ModuleSelectPage BlocListener state: $state'); }), BlocListener( listener: (context, state) { - Log.d("WQF HomePage listener state: $state"); if (state is UpdateDialogState) { _showUpdateDialog(context, state.forceUpdate, state.appVersionEntity); } @@ -97,8 +94,6 @@ class _HomePageView extends StatelessWidget { Expanded( child: BlocBuilder( builder: (context, userState) { - debugPrint( - 'WQF ModuleSelectPage BlocBuilder state: $userState'); return GestureDetector( onTap: () { _checkPermission(() { diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index 5c4e7ee..e9fbda3 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -103,7 +103,7 @@ class TopicPictureBloc extends BaseSectionBloc { - PageController _pageController; PageController get pageController => _pageController; @@ -30,10 +32,17 @@ class SectionBloc extends Bloc { ScrollController get listController => _listController; + ScrollController _indicatorSrollController; + + ScrollController get indicatorSrollController => _indicatorSrollController; + CourseUnitEntity _courseUnitEntity; CourseUnitEntity get courseUnitEntity => _courseUnitEntity; + ///单元列表是否有刷新,有的话返回上一页时通知其刷新接口数据 + bool courseUnitEntityChanged = false; + ///courseUnitId与课程环节列表的映射 final Map?> _courseSectionDatasMap = {}; @@ -45,7 +54,7 @@ class SectionBloc extends Bloc { CourseProcessEntity? get processEntity => _processEntity; SectionBloc(this._courseUnitEntity, this._currentPage, this._pageController, - this._listController) + this._listController, this._indicatorSrollController) : super(LessonInitial()) { on(_requestSectionsData); on(_requestEndClass); @@ -121,15 +130,38 @@ class SectionBloc extends Bloc { void _pageControllerChange( CurrentUnitIndexChangeEvent event, Emitter emitter) async { _currentPage = event.unitIndex; + double indicatorWidth = 30.0.w; // 指示器宽度 + + // 计算选中项的偏移量 + double offset = _currentPage * indicatorWidth - + (_indicatorSrollController.position.viewportDimension - + indicatorWidth) / + 2; + + // 确保偏移量在合理范围内 + if (offset < 0) { + offset = 0; + } else if (offset > + unlockPageCount() * indicatorWidth - + _indicatorSrollController.position.viewportDimension) { + offset = unlockPageCount() * indicatorWidth - + _indicatorSrollController.position.viewportDimension; + } + _indicatorSrollController.animateTo( + offset, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); emitter(CurrentPageIndexState()); } ///未锁定的页(单元)数 int unlockPageCount() { - return _courseUnitEntity.courseUnitVOList - ?.indexWhereOrNull((element) => element.lock == true) ?? - _courseUnitEntity.courseUnitVOList?.length ?? - 0; + // return _courseUnitEntity.courseUnitVOList + // ?.indexWhereOrNull((element) => element.lock == true) ?? + // _courseUnitEntity.courseUnitVOList?.length ?? + // 0; + return _courseUnitEntity.courseUnitVOList?.length ?? 0; } ///当前页的课程详情 @@ -172,7 +204,7 @@ class SectionBloc extends Bloc { final curCourseSectionEntity = findCourseSectionById(courseLessonId); if (curCourseSectionEntity != null) { final curCourseUnitDetail = _courseUnitEntity.courseUnitVOList - ?.firstWhere( + ?.firstWhereOrNull( (element) => element.id == curCourseSectionEntity.courseUnitId); return curCourseUnitDetail; } @@ -185,39 +217,89 @@ class SectionBloc extends Bloc { final curCourseSectionEntity = findCourseSectionById(courseLessonId); final curSectionSort = curCourseSectionEntity?.sortOrder ?? 0; - ///查找下一个section - final nextCourseSectionEntity = findCourseSectionBySort(curSectionSort + 1); - if (nextCourseSectionEntity != null) { - return nextCourseSectionEntity; + try { + ///查找当前unit的下一个section + CourseSectionEntity? nextCourseSectionEntity = + findCourseSectionBySort(curSectionSort + 1); + return checkCourseSectionLocked(courseLessonId, nextCourseSectionEntity, emitter); + } catch (e) { + if (e is ApiException) { + showToast(e.message.toString()); + } + return null; + } + } + + ///检查section是否锁定 + Future checkCourseSectionLocked(int courseLessonId, CourseSectionEntity? courseSectionEntity, + Emitter emitter) async { + if (courseSectionEntity != null) { + if (courseSectionEntity.lock == false) { + ///如果section没锁,直接返回 + return courseSectionEntity; + } else { + ///如果section锁了,请求当前unit下的section数据,查询解锁状态 + int courseUnitId = courseSectionEntity.courseUnitId; + CourseSectionEntity? result = await loading(() async { + List? tempSectionEntities = + await LessonDao.courseSection(courseUnitId: courseUnitId); + if (tempSectionEntities != null) { + _courseSectionDatasMap[courseUnitId] = tempSectionEntities; + emitter(LessonDataLoadState()); + } + courseSectionEntity = tempSectionEntities?.firstWhereOrNull( + (element) => element.id == courseSectionEntity?.id); + if (courseSectionEntity?.lock == false) { + ///刷新后的数据如果解锁了,直接返回 + return courseSectionEntity; + } else { + ///请求失败或者锁定状态没变(没变就感觉状态异常了,理论上不应该进入这条分支),返回null + showToast('下个课程还没解锁哦'); + return null; + } + }); + return result; + } } else { - ///section为空说明当前unit学完了,找下一个unit。(跨unit选lesson) + ///section为空说明当前unit学完了,找下一个unit。(跨unit选section) ///先根据courseLessonId找出当前的unit final curCourseUnitDetail = findCourseUnitDetailById(courseLessonId); if (curCourseUnitDetail != null) { - ///再根据当前unit找出下一个unit - final nextCourseUnitDetail = _courseUnitEntity.courseUnitVOList - ?.firstWhere((element) => - element.sortOrder == (curCourseUnitDetail.sortOrder! + 1)); + ///再根据当前unit的sortOrder找出下一个unit + CourseUnitDetail? nextCourseUnitDetail = + _courseUnitEntity.courseUnitVOList?.firstWhereOrNull((element) => + element.sortOrder == (curCourseUnitDetail.sortOrder! + 1)); + if (nextCourseUnitDetail != null) { - final courseUnitId = nextCourseUnitDetail.id!; - try { - await loading(() async { - _courseSectionDatasMap[courseUnitId] = - await LessonDao.courseSection(courseUnitId: courseUnitId); - emitter(LessonDataLoadState()); + if (nextCourseUnitDetail.lock == true) { + ///如果下一个unit是锁定状态,请求数据刷新查询解锁状态 + CourseSectionEntity? result = await loading(() async { + CourseUnitEntity? newCourseUnitEntity = await LessonDao.courseUnit( + _courseUnitEntity.nowCourseModuleId); + + ///拿到重新获取到的unit后,再次判断是否解锁 + nextCourseUnitDetail = newCourseUnitEntity?.courseUnitVOList?.firstWhereOrNull( + (element) => element.id == nextCourseUnitDetail?.id); + if (nextCourseUnitDetail?.lock == false) { + ///解锁状态从锁定到解锁,覆盖原unit数据并刷新ui + _courseUnitEntity = newCourseUnitEntity!; + courseUnitEntityChanged = true; + emitter(LessonDataLoadState()); + + return checkCourseSectionLockedOfNextUnit(courseLessonId, nextCourseUnitDetail!.id!, emitter); + } else { + showToast('下个单元课程还没解锁哦'); + + ///如果还是锁定状态,返回null + return null; + } }); - _pageController.nextPage( - duration: const Duration(milliseconds: 500), - curve: Curves.ease, - ); - return _courseSectionDatasMap[courseUnitId]!.first; - } catch (e) { - if (e is ApiException) { - showToast(e.message.toString()); - } - return null; + return result; + } else { + return checkCourseSectionLockedOfNextUnit(courseLessonId, nextCourseUnitDetail.id!, emitter); } } else { + showToast("恭喜你,本阶段学到顶啦"); ///最后一个unit了 return null; } @@ -227,4 +309,43 @@ class SectionBloc extends Bloc { } } } + + ///检查下一个unit的(第一个)section + Future checkCourseSectionLockedOfNextUnit(int courseLessonId, int nextCourseUnitDetailId, + Emitter emitter) async { + CourseSectionEntity? firstSectionNextUnit = await getFirstSectionByUnitId( + nextCourseUnitDetailId, emitter); + if (firstSectionNextUnit != null) { + ///下个unit的第一个section如果不为空,再次检查是否锁定 + CourseSectionEntity? courseSectionEntity = await checkCourseSectionLocked(courseLessonId, firstSectionNextUnit, emitter); + if (courseSectionEntity != null) { + ///只有是下一unit的第一个section并且解锁了,才跳转 + _pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + ); + } + return courseSectionEntity; + } else { + ///下个unit的第一个section如果为空,返回null + showToast('下个课程暂未找到'); + return null; + } + } + + ///根据unitId获取当前unit的第一个section + Future getFirstSectionByUnitId( + int courseUnitId, Emitter emitter) async { + List? courseSectionEntity = + _courseSectionDatasMap[courseUnitId]; + if (courseSectionEntity == null) { + ///如果没下载过,请求数据 + await loading(() async { + _courseSectionDatasMap[courseUnitId] = + await LessonDao.courseSection(courseUnitId: courseUnitId); + emitter(LessonDataLoadState()); + }); + } + return _courseSectionDatasMap[courseUnitId]?.first; + } } diff --git a/lib/pages/section/section_page.dart b/lib/pages/section/section_page.dart index 1db876e..8adbc11 100644 --- a/lib/pages/section/section_page.dart +++ b/lib/pages/section/section_page.dart @@ -33,8 +33,14 @@ class SectionPage extends StatelessWidget { ?.indexWhere((element) => element.id == courseUnitId) ?? 0; return BlocProvider( - create: (context) => SectionBloc(courseUnitEntity, initialPage, - PageController(initialPage: initialPage), ScrollController()), + create: (context) => SectionBloc( + courseUnitEntity, + initialPage, + PageController(initialPage: initialPage), + ScrollController(), + ScrollController()), + //为了触发指示器进入后计算位置 + // ..add(CurrentUnitIndexChangeEvent(initialPage)), child: _SectionPageView(context), ); } @@ -146,78 +152,86 @@ class _SectionPageView extends StatelessWidget { children: [ SectionHeaderWidget( title: bloc.getCourseUnitDetail().name, - courseModuleCode: bloc.courseUnitEntity.courseModuleCode), + courseModuleCode: bloc.courseUnitEntity.courseModuleCode, + onBack: () { + popPage(data: { + 'needRefresh': bloc.courseUnitEntityChanged, + }); + }), Expanded( - child: Container( - color: Colors.blue, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 10.w), - // child: OverflowBox( - child: NestedPageView.builder( - itemCount: bloc.unlockPageCount(), - controller: bloc.pageController, - onPageChanged: (int index) { - bloc.add(CurrentUnitIndexChangeEvent(index)); - }, - itemBuilder: (context, index) { - // return ScrollConfiguration( - // ///去掉 Android 上默认的边缘拖拽效果 - // behavior: ScrollConfiguration.of(context) - // .copyWith(overscroll: false), - // child: _itemTransCard( - // bloc.getCourseUnitDetail(pageIndex: index), - // index, - // context), - // ); - return _itemTransCard( - bloc.getCourseUnitDetail(pageIndex: index), - index, - context); - }), - // ), // 设置外部padding, - ) - ) - ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: NestedPageView.builder( + itemCount: bloc.unlockPageCount(), + controller: bloc.pageController, + onPageChanged: (int index) { + bloc.add(CurrentUnitIndexChangeEvent(index)); + }, + itemBuilder: (context, index) { + return ScrollConfiguration( + ///去掉 Android 上默认的边缘拖拽效果 + behavior: ScrollConfiguration.of(context) + .copyWith(overscroll: false), + child: _itemTransCard( + bloc.getCourseUnitDetail(pageIndex: index), + index, + context), + ); + }), + )), SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 13.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - height: 47.h, - width: 80.w, - ), - Container( - decoration: BoxDecoration( - color: CourseModuleModel( - bloc.courseUnitEntity.courseModuleCode ?? - 'Phase-1') - .color, - borderRadius: BorderRadius.circular(14.5.r), - ), - padding: EdgeInsets.symmetric( - vertical: 8.h, horizontal: 24.w), - child: Text( - '${bloc.currentPage + 1}/${bloc.unlockPageCount()}', - style: TextStyle( - color: Colors.white, fontSize: 12.sp), - ), - ), - Image.asset( - CourseModuleModel( - bloc.courseUnitEntity.courseModuleCode ?? - 'Phase-1') - .courseModuleLogo - .assetPng, - height: 47.h, - width: 80.w, - // color: Colors.red, - ), - ], - ), - ), - ) + child: SizedBox( + width: 210.w, + height: 40.h, + // child: OverflowBox( + // maxWidth: 300, // 允许内容超出容器宽度 + child: ListView.builder( + controller: bloc.indicatorSrollController, + scrollDirection: Axis.horizontal, + itemCount: bloc.unlockPageCount(), + itemBuilder: (context, index) { + bool isSelected = index == bloc.currentPage; + // 计算透明度,使得当前页的指示器不透明,两侧逐渐透明 + double opacity = 0.25 + + (1 - ((bloc.currentPage - index).abs() / 2)) * + 0.5; + // 限制透明度在 0.5 到 1.0 之间 + opacity = opacity.clamp(0.25, 1.0); + return GestureDetector( + onTap: () => bloc.pageController.animateToPage( + index, + duration: const Duration(milliseconds: 60), + curve: Curves.ease), + child: SizedBox( + width: 30.0.w, + height: 30.0.h, + child: Padding( + padding: + EdgeInsets.all(isSelected ? 0.w : 5.w), + child: Opacity( + opacity: opacity, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue + : Colors.grey, + shape: BoxShape.circle, + ), + child: Text("${index + 1}", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: + isSelected ? 12.sp : 8.sp)), + ), + ), + ), + ), + ); + })), + ), + // ) ], ), ), @@ -252,62 +266,54 @@ Widget _itemTransCard( ), ); } else { - return - Padding(padding: EdgeInsets.symmetric( - vertical: 28.h), - child: Container( - color: Colors.red, - margin: EdgeInsets.symmetric(vertical: 10.h), - child: NestedListView.builder( - itemCount: bloc.courseSectionDatasMap[courseUnitDetail.id]?.length ?? 0, - scrollDirection: Axis.horizontal, - itemBuilder: (BuildContext context, int index) { - CourseSectionEntity sectionData = courseSectionEntities[index]; - if (sectionData.courseType == SectionType.bouns.value) { - //彩蛋 - return GestureDetector( - onTap: () { - if (!UserUtil.isLogined()) { - pushNamed(AppRouteName.login); - return; - } - if (sectionData.lock == true) { - showToast('当前课程暂未解锁'); - return; - } + return NestedListView.builder( + itemCount: bloc.courseSectionDatasMap[courseUnitDetail.id]?.length ?? 0, + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int index) { + CourseSectionEntity sectionData = courseSectionEntities[index]; + if (sectionData.courseType == SectionType.bouns.value) { + //彩蛋 + return GestureDetector( + onTap: () { + if (!UserUtil.isLogined()) { + pushNamed(AppRouteName.login); + return; + } + if (sectionData.lock == true) { + showToast('当前课程暂未解锁'); + return; + } - ///进入课堂 - bloc.add(RequestEnterClassEvent( - sectionData.id.toString(), sectionData.courseType)); - }, - child: SectionBoundsItem( - imageUrl: sectionData.coverUrl, - ), - ); - } else { - return GestureDetector( - onTap: () { - if (!UserUtil.isLogined()) { - pushNamed(AppRouteName.login); - return; - } - if (sectionData.lock == true) { - showToast('当前课程暂未解锁'); - return; - } + ///进入课堂 + bloc.add(RequestEnterClassEvent( + sectionData.id.toString(), sectionData.courseType)); + }, + child: SectionBoundsItem( + imageUrl: sectionData.coverUrl, + ), + ); + } else { + return GestureDetector( + onTap: () { + if (!UserUtil.isLogined()) { + pushNamed(AppRouteName.login); + return; + } + if (sectionData.lock == true) { + showToast('当前课程暂未解锁'); + return; + } - ///进入课堂 - bloc.add(RequestEnterClassEvent( - sectionData.id.toString(), sectionData.courseType)); - }, - child: SectionItem( - courseModuleId: bloc.courseUnitEntity.courseModuleCode, - lessons: sectionData, - ), - ); - } - }), - ), - ); + ///进入课堂 + bloc.add(RequestEnterClassEvent( + sectionData.id.toString(), sectionData.courseType)); + }, + child: SectionItem( + courseModuleId: bloc.courseUnitEntity.courseModuleCode, + lessons: sectionData, + ), + ); + } + }); } } diff --git a/lib/pages/section/widgets/section_header_widget.dart b/lib/pages/section/widgets/section_header_widget.dart index 91833df..54eb6d3 100644 --- a/lib/pages/section/widgets/section_header_widget.dart +++ b/lib/pages/section/widgets/section_header_widget.dart @@ -7,12 +7,14 @@ import 'package:wow_english/pages/user/bloc/user_bloc.dart'; import '../courese_module_model.dart'; class SectionHeaderWidget extends StatelessWidget { - const SectionHeaderWidget({super.key, this.title, this.courseModuleCode}); + const SectionHeaderWidget({super.key, this.title, this.courseModuleCode, this.onBack}); final String? title; final String? courseModuleCode; + final VoidCallback? onBack; + @override Widget build(BuildContext context) { return BlocBuilder( @@ -27,7 +29,11 @@ class SectionHeaderWidget extends StatelessWidget { ScreenUtil().bottomBarHeight.horizontalSpace, GestureDetector( onTap: () { - Navigator.pop(context); + if (onBack == null) { + Navigator.pop(context); + } else { + onBack!(); + } }, child: Container( alignment: Alignment.center, diff --git a/lib/pages/unit/bloc.dart b/lib/pages/unit/bloc.dart index d54cd60..b791c82 100644 --- a/lib/pages/unit/bloc.dart +++ b/lib/pages/unit/bloc.dart @@ -23,10 +23,10 @@ class UnitBloc extends Bloc { UnitBloc(CourseModuleEntity? courseEntity) : super(UnitState().init()) { - on(_requestData); + on(_requestUnitDatas); } - void _requestData(RequestUnitDataEvent event, Emitter emitter) async { + void _requestUnitDatas(RequestUnitDataEvent event, Emitter emitter) async { try { await loading(() async { _unitData = await LessonDao.courseUnit(event.moduleId); diff --git a/lib/pages/unit/view.dart b/lib/pages/unit/view.dart index bf82ee3..ed7f07c 100644 --- a/lib/pages/unit/view.dart +++ b/lib/pages/unit/view.dart @@ -57,7 +57,7 @@ class UnitPage extends StatelessWidget { return GestureDetector( onTap: () { if (data.lock == true) { - showToast('当前课程暂未解锁'); + showToast('当前单元课程暂未解锁'); return; } @@ -65,7 +65,15 @@ class UnitPage extends StatelessWidget { arguments: { 'courseUnitEntity': bloc.unitData, 'courseUnitId': data.id - }); + }).then((value) { + if (value != null) { + Map dataMap = value as Map; + bool needRefresh = dataMap['needRefresh']; + if (needRefresh) { + bloc.add(RequestUnitDataEvent(courseModuleEntity?.id)); + } + } + }); }, child: CourseUnitItem( unitEntity: bloc.unitData!, diff --git a/lib/utils/list_ext.dart b/lib/utils/list_ext.dart index c7ac32a..3eebf59 100644 --- a/lib/utils/list_ext.dart +++ b/lib/utils/list_ext.dart @@ -8,4 +8,14 @@ extension ListExtension on List { } return null; } + + /// 获取数组中第一个匹配元素,没有就返回null + E? firstWhereOrNull(bool Function(E element) test) { + for (E element in this) { + if (test(element)) { + return element; + } + } + return null; + } }