diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index e49a9ac2a..b86a506e2 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class ActivityPlanModel { final String activityId; + final ActivityPlanRequest req; final String title; final String description; @@ -66,6 +67,7 @@ class ActivityPlanModel { ); } + final activityId = json[ModelKey.activityId] ?? json["bookmark_id"]; return ActivityPlanModel( imageURL: json[ModelKey.activityPlanImageURL], instructions: json[ModelKey.activityPlanInstructions], @@ -88,7 +90,7 @@ class ActivityPlanModel { ) : null, roles: roles, - activityId: json[ModelKey.activityId] ?? json["bookmark_id"], + activityId: activityId, ); } diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index f2955dd82..2e0866110 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -18,7 +18,7 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart index a3783f492..7d9836d37 100644 --- a/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart +++ b/lib/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart @@ -12,11 +12,10 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/bot_join_error_dialog.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.dart'; -import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -59,7 +58,6 @@ class ActivitySessionStartPage extends StatefulWidget { class ActivitySessionStartController extends State with ActivitySummariesProvider { ActivityPlanModel? activity; - CoursePlanModel? course; bool loading = true; Object? error; @@ -264,14 +262,14 @@ class ActivitySessionStartController extends State } Future _loadActivity() async { - if (courseParent?.coursePlan != null) { - course = await CoursePlansRepo.get(courseParent!.coursePlan!.uuid); - } - - final activities = await CourseActivityRepo.get( + final activitiesResponse = await CourseActivityRepo.get( + TranslateActivityRequest( + activityIds: [widget.activityId], + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), widget.activityId, - [widget.activityId], ); + final activities = activitiesResponse.plans.values.toList(); if (activities.isEmpty) { throw Exception("Activity not found"); diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart index dfe694501..911aa2510 100644 --- a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart +++ b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart @@ -11,7 +11,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activ import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -94,6 +94,8 @@ class ActivitySessionStartView extends StatelessWidget { CourseActivityRepo.setSentFeedback( controller.widget.activityId, + MatrixState.pangeaController.languageController + .activeL1Code()!, ); await showDialog( diff --git a/lib/pangea/chat_settings/pages/space_details_content.dart b/lib/pangea/chat_settings/pages/space_details_content.dart index ac308b6f6..ad8094dda 100644 --- a/lib/pangea/chat_settings/pages/space_details_content.dart +++ b/lib/pangea/chat_settings/pages/space_details_content.dart @@ -15,8 +15,7 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart import 'package:fluffychat/pangea/common/widgets/share_room_button.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_plans/map_clipper.dart'; import 'package:fluffychat/pangea/course_settings/course_settings.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; @@ -204,159 +203,153 @@ class SpaceDetailsContent extends StatelessWidget { final displayname = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)), ); - return CoursePlanBuilder( - courseId: room.coursePlan?.uuid, - builder: (context, courseController) { - return Column( - mainAxisSize: MainAxisSize.min, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: isColumnMode + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - crossAxisAlignment: isColumnMode - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isColumnMode) ...[ - ClipPath( - clipper: MapClipper(), - child: Avatar( - mxContent: room.avatar, - name: displayname, - userId: room.directChatMatrixID, - size: 80.0, - borderRadius: BorderRadius.circular(0.0), + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isColumnMode) ...[ + ClipPath( + clipper: MapClipper(), + child: Avatar( + mxContent: room.avatar, + name: displayname, + userId: room.directChatMatrixID, + size: 80.0, + borderRadius: BorderRadius.circular(0.0), + ), + ), + const SizedBox(width: 16.0), + ], + Flexible( + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isColumnMode ? 32.0 : 16.0, + fontWeight: isColumnMode + ? FontWeight.normal + : FontWeight.bold, ), ), - const SizedBox(width: 16.0), + if (isColumnMode && room.coursePlan != null) + CourseInfoChips( + room.coursePlan!.uuid, + fontSize: 12.0, + iconSize: 12.0, + ), ], - Flexible( - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: isColumnMode ? 32.0 : 16.0, - fontWeight: isColumnMode - ? FontWeight.normal - : FontWeight.bold, - ), - ), - if (isColumnMode && courseController.course != null) - CourseInfoChips( - courseController.course!, - fontSize: 12.0, - iconSize: 12.0, - ), - ], - ), - ), - ], + ), ), - ), - if (room.classCode != null) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: ShareRoomButton(room: room), - ), - ], - ), - SizedBox(height: isColumnMode ? 24.0 : 12.0), - SpaceDetailsButtonRow( - controller: controller, - room: room, - selectedTab: tab(context), - onTabSelected: (tab) => setSelectedTab(tab, context), - buttons: _buttons(context), - ), - SizedBox(height: isColumnMode ? 30.0 : 14.0), - Expanded( - child: Builder( - builder: (context) { - switch (tab(context)) { - case SpaceSettingsTabs.chat: - return CourseChats( - room.id, - activeChat: null, - client: room.client, - ); - case SpaceSettingsTabs.course: - return SingleChildScrollView( - child: CourseSettings( - // on redirect back to chat settings after completing activity, - // course settings doesn't refresh activity details by default - // the key forces a rebuild on this redirect - key: ValueKey(controller.widget.activeTab), - courseController, - room: room, - ), - ); - case SpaceSettingsTabs.participants: - return SingleChildScrollView( - child: RoomParticipantsSection(room: room), - ); - case SpaceSettingsTabs.analytics: - return SingleChildScrollView( - child: Center( - child: SpaceAnalytics(roomId: room.id), - ), - ); - case SpaceSettingsTabs.more: - final buttons = _buttons(context) - .where( - (b) => !b.showInMainView && b.visible, - ) - .toList(); - - return SingleChildScrollView( - child: Column( - children: [ - if (room.topic.isNotEmpty) ...[ - Text( - room.topic, - style: TextStyle( - fontSize: isColumnMode ? 16.0 : 12.0, - ), - ), - SizedBox(height: isColumnMode ? 30.0 : 14.0), - ], - Column( - spacing: 10.0, - mainAxisSize: MainAxisSize.min, - children: buttons.map((b) { - return Opacity( - opacity: b.enabled ? 1.0 : 0.5, - child: ListTile( - title: Text(b.title), - subtitle: b.description != null - ? Text(b.description!) - : null, - leading: b.icon, - onTap: b.enabled - ? () => b.onPressed?.call() - : null, - ), - ); - }).toList(), - ), - ], - ), - ); - } - }, + ], ), ), + if (room.classCode != null) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: ShareRoomButton(room: room), + ), ], - ); - }, + ), + SizedBox(height: isColumnMode ? 24.0 : 12.0), + SpaceDetailsButtonRow( + controller: controller, + room: room, + selectedTab: tab(context), + onTabSelected: (tab) => setSelectedTab(tab, context), + buttons: _buttons(context), + ), + SizedBox(height: isColumnMode ? 30.0 : 14.0), + Expanded( + child: Builder( + builder: (context) { + switch (tab(context)) { + case SpaceSettingsTabs.chat: + return CourseChats( + room.id, + activeChat: null, + client: room.client, + ); + case SpaceSettingsTabs.course: + return SingleChildScrollView( + child: CourseSettings( + // on redirect back to chat settings after completing activity, + // course settings doesn't refresh activity details by default + // the key forces a rebuild on this redirect + key: ValueKey(controller.widget.activeTab), + room: room, + ), + ); + case SpaceSettingsTabs.participants: + return SingleChildScrollView( + child: RoomParticipantsSection(room: room), + ); + case SpaceSettingsTabs.analytics: + return SingleChildScrollView( + child: Center( + child: SpaceAnalytics(roomId: room.id), + ), + ); + case SpaceSettingsTabs.more: + final buttons = _buttons(context) + .where( + (b) => !b.showInMainView && b.visible, + ) + .toList(); + + return SingleChildScrollView( + child: Column( + children: [ + if (room.topic.isNotEmpty) ...[ + Text( + room.topic, + style: TextStyle( + fontSize: isColumnMode ? 16.0 : 12.0, + ), + ), + SizedBox(height: isColumnMode ? 30.0 : 14.0), + ], + Column( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: buttons.map((b) { + return Opacity( + opacity: b.enabled ? 1.0 : 0.5, + child: ListTile( + title: Text(b.title), + subtitle: b.description != null + ? Text(b.description!) + : null, + leading: b.icon, + onTap: b.enabled + ? () => b.onPressed?.call() + : null, + ), + ); + }).toList(), + ), + ], + ), + ); + } + }, + ), + ), + ], ); } } diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index a98a05405..a9b027e01 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -73,6 +73,14 @@ class PApiUrls { static String constructSummary = "${PApiUrls._choreoEndpoint}/construct_summary"; + ///--------------------------- course translations --------------------------- + static String getLocalizedCourse = + "${PApiUrls._choreoEndpoint}/course_plans/localize"; + static String getLocalizedTopic = + "${PApiUrls._choreoEndpoint}/topics/localize"; + static String getLocalizedActivity = + "${PApiUrls._choreoEndpoint}/activity_plan/localize"; + ///-------------------------------- revenue cat -------------------------- static String rcAppsChoreo = "${PApiUrls._subscriptionEndpoint}/app_ids"; static String rcProductsChoreo = diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index f89792e5a..a3bb9b4c6 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -15,8 +15,7 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_view.dart'; import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.dart'; -import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -53,8 +52,6 @@ class CourseChatsController extends State bool noMoreRooms = false; bool isLoading = false; - CoursePlanModel? course; - @override void initState() { loadHierarchy(reload: true); @@ -93,12 +90,6 @@ class CourseChatsController extends State super.dispose(); } - void setCourse(CoursePlanModel? course) { - setState(() { - this.course = course; - }); - } - Set get childrenIds => room?.spaceChildren.map((c) => c.roomId).whereType().toSet() ?? {}; @@ -148,7 +139,6 @@ class CourseChatsController extends State sessionsMap[activity]!.add( ExtendedSpaceRoomsChunk( chunk: chunk, - activityId: activity.activityId, userIds: users, ), ); diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index 7aa598caa..99f077c15 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -13,8 +13,6 @@ import 'package:fluffychat/pangea/chat_settings/widgets/chat_context_menu_action import 'package:fluffychat/pangea/course_chats/activity_template_chat_list_item.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/space_analytics/analytics_request_indicator.dart'; import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart'; import 'package:fluffychat/utils/stream_extension.dart'; @@ -38,211 +36,204 @@ class CourseChatsView extends StatelessWidget { ); } - return CoursePlanBuilder( - courseId: room.coursePlan?.uuid, - onLoaded: controller.setCourse, - builder: (context, courseController) { - return StreamBuilder( - stream: room.client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) { - final joinedChats = controller.joinedChats; - final joinedSessions = controller.joinedActivities(); + return StreamBuilder( + stream: room.client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + final joinedChats = controller.joinedChats; + final joinedSessions = controller.joinedActivities(); - final discoveredGroupChats = controller.discoveredGroupChats; - final discoveredSessions = - controller.discoveredActivities().entries.toList(); + final discoveredGroupChats = controller.discoveredGroupChats; + final discoveredSessions = + controller.discoveredActivities().entries.toList(); - final isColumnMode = FluffyThemes.isColumnMode(context); - return Padding( - padding: isColumnMode - ? const EdgeInsets.symmetric( - vertical: 12.0, + final isColumnMode = FluffyThemes.isColumnMode(context); + return Padding( + padding: isColumnMode + ? const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 8.0, + ) + : const EdgeInsets.all(0.0), + child: ListView.builder( + shrinkWrap: true, + itemCount: joinedChats.length + + joinedSessions.length + + discoveredGroupChats.length + + discoveredSessions.length + + 7, + itemBuilder: (context, i) { + // courses chats title + if (i == 0) { + if (isColumnMode) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 4.0, horizontal: 8.0, - ) - : const EdgeInsets.all(0.0), - child: ListView.builder( - shrinkWrap: true, - itemCount: joinedChats.length + - joinedSessions.length + - discoveredGroupChats.length + - discoveredSessions.length + - 7, - itemBuilder: (context, i) { - // courses chats title - if (i == 0) { - if (isColumnMode) { - return const Padding( - padding: EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - LearningProgressIndicators(), - Icon( - Icons.chat_bubble_outline, - size: 30.0, - ), - SizedBox(height: 12.0), - ], - ), - ); - } - - return const SizedBox(); - } - i--; - - if (i == 0) { - return KnockingUsersIndicator(room: room); - } - i--; - - if (i == 0) { - return AnalyticsRequestIndicator(room: room); - } - i--; - - // joined group chats - if (i < joinedChats.length) { - final joinedRoom = joinedChats[i]; - return ChatListItem( - joinedRoom, - onTap: () => controller.onChatTap(joinedRoom), - onLongPress: (c) => chatContextMenuAction( - joinedRoom, - c, - context, - () => controller.onChatTap(joinedRoom), - ), - activeChat: controller.widget.activeChat == joinedRoom.id, - ); - } - i -= joinedChats.length; - - // unjoined group chats - if (i < discoveredGroupChats.length) { - return UnjoinedChatListItem( - chunk: discoveredGroupChats[i], - onTap: () => - controller.joinChildRoom(discoveredGroupChats[i]), - ); - } - i -= discoveredGroupChats.length; - - if (i == 0) { - return joinedSessions.isEmpty && discoveredSessions.isEmpty - ? ListTile( - leading: const Icon(Icons.map_outlined), - title: Text(L10n.of(context).whatNow), - subtitle: Text(L10n.of(context).chooseNextActivity), - trailing: const Icon(Icons.arrow_forward), - onTap: () => context.go( - "/rooms/spaces/${room.id}/details?tab=course", - ), - ) - : const SizedBox(); - } - i--; - - if (i == 0) { - return joinedSessions.isEmpty - ? const SizedBox() - : Padding( - padding: const EdgeInsets.only( - top: 20.0, - bottom: 4.0, - ), - child: Text( - L10n.of(context).myActivities, - style: const TextStyle(fontSize: 12.0), - textAlign: TextAlign.center, - ), - ); - } - i--; - - // joined activity sessions - if (i < joinedSessions.length) { - final joinedRoom = joinedSessions[i]; - return ChatListItem( - joinedRoom, - onTap: () => controller.onChatTap(joinedRoom), - onLongPress: (c) => chatContextMenuAction( - joinedRoom, - c, - context, - () => controller.onChatTap(joinedRoom), - ), - activeChat: controller.widget.activeChat == joinedRoom.id, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, - ), - ); - } - i -= joinedSessions.length; - - if (i == 0) { - return discoveredSessions.isEmpty - ? const SizedBox() - : Padding( - padding: const EdgeInsets.only( - top: 20.0, - bottom: 4.0, - ), - child: Text( - L10n.of(context).openToJoin, - style: const TextStyle(fontSize: 12.0), - textAlign: TextAlign.center, - ), - ); - } - i--; - - // unjoined activity sessions - if (i < discoveredSessions.length) { - final activity = discoveredSessions[i].key; - final sessions = discoveredSessions[i].value; - return ActivityTemplateChatListItem( - space: room, - activity: activity, - sessions: sessions, - joinActivity: (e) => - controller.joinActivity(activity.activityId, e), - ); - } - i -= discoveredSessions.length; - - if (controller.noMoreRooms) { - return const SizedBox(); - } - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, ), - child: TextButton( - onPressed: controller.isLoading - ? null - : controller.loadHierarchy, - child: controller.isLoading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ) - : Text(L10n.of(context).loadMore), + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + LearningProgressIndicators(), + Icon( + Icons.chat_bubble_outline, + size: 30.0, + ), + SizedBox(height: 12.0), + ], ), ); - }, - ), - ); - }, + } + + return const SizedBox(); + } + i--; + + if (i == 0) { + return KnockingUsersIndicator(room: room); + } + i--; + + if (i == 0) { + return AnalyticsRequestIndicator(room: room); + } + i--; + + // joined group chats + if (i < joinedChats.length) { + final joinedRoom = joinedChats[i]; + return ChatListItem( + joinedRoom, + onTap: () => controller.onChatTap(joinedRoom), + onLongPress: (c) => chatContextMenuAction( + joinedRoom, + c, + context, + () => controller.onChatTap(joinedRoom), + ), + activeChat: controller.widget.activeChat == joinedRoom.id, + ); + } + i -= joinedChats.length; + + // unjoined group chats + if (i < discoveredGroupChats.length) { + return UnjoinedChatListItem( + chunk: discoveredGroupChats[i], + onTap: () => + controller.joinChildRoom(discoveredGroupChats[i]), + ); + } + i -= discoveredGroupChats.length; + + if (i == 0) { + return joinedSessions.isEmpty && discoveredSessions.isEmpty + ? ListTile( + leading: const Icon(Icons.map_outlined), + title: Text(L10n.of(context).whatNow), + subtitle: Text(L10n.of(context).chooseNextActivity), + trailing: const Icon(Icons.arrow_forward), + onTap: () => context.go( + "/rooms/spaces/${room.id}/details?tab=course", + ), + ) + : const SizedBox(); + } + i--; + + if (i == 0) { + return joinedSessions.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only( + top: 20.0, + bottom: 4.0, + ), + child: Text( + L10n.of(context).myActivities, + style: const TextStyle(fontSize: 12.0), + textAlign: TextAlign.center, + ), + ); + } + i--; + + // joined activity sessions + if (i < joinedSessions.length) { + final joinedRoom = joinedSessions[i]; + return ChatListItem( + joinedRoom, + onTap: () => controller.onChatTap(joinedRoom), + onLongPress: (c) => chatContextMenuAction( + joinedRoom, + c, + context, + () => controller.onChatTap(joinedRoom), + ), + activeChat: controller.widget.activeChat == joinedRoom.id, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + ); + } + i -= joinedSessions.length; + + if (i == 0) { + return discoveredSessions.isEmpty + ? const SizedBox() + : Padding( + padding: const EdgeInsets.only( + top: 20.0, + bottom: 4.0, + ), + child: Text( + L10n.of(context).openToJoin, + style: const TextStyle(fontSize: 12.0), + textAlign: TextAlign.center, + ), + ); + } + i--; + + // unjoined activity sessions + if (i < discoveredSessions.length) { + final activity = discoveredSessions[i].key; + final sessions = discoveredSessions[i].value; + return ActivityTemplateChatListItem( + space: room, + activity: activity, + sessions: sessions, + joinActivity: (e) => + controller.joinActivity(activity.activityId, e), + ); + } + i -= discoveredSessions.length; + + if (controller.noMoreRooms) { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, + ), + child: TextButton( + onPressed: + controller.isLoading ? null : controller.loadHierarchy, + child: controller.isLoading + ? LinearProgressIndicator( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ) + : Text(L10n.of(context).loadMore), + ), + ); + }, + ), ); }, ); diff --git a/lib/pangea/course_chats/extended_space_rooms_chunk.dart b/lib/pangea/course_chats/extended_space_rooms_chunk.dart index 8006f4f3f..f2621a0e7 100644 --- a/lib/pangea/course_chats/extended_space_rooms_chunk.dart +++ b/lib/pangea/course_chats/extended_space_rooms_chunk.dart @@ -2,12 +2,10 @@ import 'package:matrix/matrix.dart'; class ExtendedSpaceRoomsChunk { final SpaceRoomsChunk chunk; - final String activityId; final List userIds; ExtendedSpaceRoomsChunk({ required this.chunk, - required this.activityId, required this.userIds, }); } diff --git a/lib/pangea/course_creation/course_info_chip_widget.dart b/lib/pangea/course_creation/course_info_chip_widget.dart index 0c477de39..ddcbed2bc 100644 --- a/lib/pangea/course_creation/course_info_chip_widget.dart +++ b/lib/pangea/course_creation/course_info_chip_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; class CourseInfoChip extends StatelessWidget { @@ -45,22 +45,46 @@ class CourseInfoChip extends StatelessWidget { } } -class CourseInfoChips extends StatelessWidget { - final CoursePlanModel course; +class CourseInfoChips extends StatefulWidget { + final String courseId; final double? fontSize; final double? iconSize; final EdgeInsets? padding; const CourseInfoChips( - this.course, { + this.courseId, { super.key, this.fontSize, this.iconSize, this.padding, }); + @override + State createState() => CourseInfoChipsState(); +} + +class CourseInfoChipsState extends State + with CoursePlanProvider { + @override + void initState() { + super.initState(); + loadCourse(widget.courseId); + } + + @override + void didUpdateWidget(covariant CourseInfoChips oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.courseId != widget.courseId) { + loadCourse(widget.courseId); + } + } + @override Widget build(BuildContext context) { + if (course == null) { + return const SizedBox.shrink(); + } + return Wrap( spacing: 8.0, runSpacing: 8.0, @@ -69,31 +93,24 @@ class CourseInfoChips extends StatelessWidget { CourseInfoChip( icon: Icons.language, text: - "${course.baseLanguageDisplay} → ${course.targetLanguageDisplay}", - fontSize: fontSize, - iconSize: iconSize, - padding: padding, + "${course!.baseLanguageDisplay} → ${course!.targetLanguageDisplay}", + fontSize: widget.fontSize, + iconSize: widget.iconSize, + padding: widget.padding, ), CourseInfoChip( icon: Icons.school, - text: course.cefrLevel.string, - fontSize: fontSize, - iconSize: iconSize, - padding: padding, + text: course!.cefrLevel.string, + fontSize: widget.fontSize, + iconSize: widget.iconSize, + padding: widget.padding, ), CourseInfoChip( icon: Icons.location_on, - text: L10n.of(context).numModules(course.topicIds.length), - fontSize: fontSize, - iconSize: iconSize, - padding: padding, - ), - CourseInfoChip( - icon: Icons.event_note_outlined, - text: L10n.of(context).numActivityPlans(course.totalActivities), - fontSize: fontSize, - iconSize: iconSize, - padding: padding, + text: L10n.of(context).numModules(course!.topicIds.length), + fontSize: widget.fontSize, + iconSize: widget.iconSize, + padding: widget.padding, ), ], ); diff --git a/lib/pangea/course_creation/course_invite_page.dart b/lib/pangea/course_creation/course_invite_page.dart index 034220751..eeacef626 100644 --- a/lib/pangea/course_creation/course_invite_page.dart +++ b/lib/pangea/course_creation/course_invite_page.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -29,7 +29,22 @@ class CourseInvitePage extends StatefulWidget { CourseInvitePageController createState() => CourseInvitePageController(); } -class CourseInvitePageController extends State { +class CourseInvitePageController extends State + with CoursePlanProvider { + @override + void initState() { + super.initState(); + loadCourse(widget.courseId); + } + + @override + void didUpdateWidget(covariant CourseInvitePage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.courseId != widget.courseId) { + loadCourse(widget.courseId); + } + } + Future getSpaceId() async { if (widget.courseCreationCompleter == null) { throw Exception("No course creation completer provided"); @@ -44,184 +59,176 @@ class CourseInvitePageController extends State { final theme = Theme.of(context); final client = Matrix.of(context).client; - return CoursePlanBuilder( - courseId: widget.courseId, - builder: (context, courseController) { - return Scaffold( - body: SafeArea( - child: Center( - child: Container( - padding: const EdgeInsets.all(30.0), - constraints: const BoxConstraints( - maxWidth: 750, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - courseController.course != null - ? Container( - decoration: BoxDecoration( - border: Border.all(color: AppConfig.gold), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 16.0, - mainAxisSize: MainAxisSize.min, + return Scaffold( + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 750, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + course != null + ? Container( + decoration: BoxDecoration( + border: Border.all(color: AppConfig.gold), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - spacing: 10.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.map_outlined, - size: 40.0, - ), - Flexible( - child: Text( - courseController.course!.title, - style: theme.textTheme.titleLarge - ?.copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], + const Icon( + Icons.map_outlined, + size: 40.0, ), - CourseInfoChips( - courseController.course!, - fontSize: 12.0, - iconSize: 12.0, + Flexible( + child: Text( + course!.title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), ), ], ), - ) - : const CircularProgressIndicator.adaptive(), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - spacing: 16.0, - mainAxisSize: MainAxisSize.min, - children: [ - LayoutBuilder( - builder: (context, constraints) { - const avatarSpace = avatarSize + 8.0; - final availableSpace = - constraints.maxWidth - 24.0; + CourseInfoChips( + widget.courseId, + fontSize: 12.0, + iconSize: 12.0, + ), + ], + ), + ) + : loadingCourse + ? const CircularProgressIndicator.adaptive() + : const SizedBox(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + LayoutBuilder( + builder: (context, constraints) { + const avatarSpace = avatarSize + 8.0; + final availableSpace = constraints.maxWidth - 24.0; - final visibleAvatars = min( - 3, - (availableSpace / avatarSpace).floor() - 2, - ); + final visibleAvatars = min( + 3, + (availableSpace / avatarSpace).floor() - 2, + ); - return Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: client - .getProfileFromUserId(client.userID!), - builder: (context, snapshot) { - return Avatar( - size: avatarSize, - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - client.userID!.localpart, - userId: client.userID!, - ); - }, - ), - Avatar( - userId: BotName.byEnvironment, + return Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: + client.getProfileFromUserId(client.userID!), + builder: (context, snapshot) { + return Avatar( size: avatarSize, + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + client.userID!.localpart, + userId: client.userID!, + ); + }, + ), + Avatar( + userId: BotName.byEnvironment, + size: avatarSize, + ), + ...List.generate(visibleAvatars, (index) { + return CircleAvatar( + radius: avatarSize / 2, + backgroundColor: AppConfig.gold.withAlpha(80), + child: const Icon( + Icons.question_mark, + size: 20.0, ), - ...List.generate(visibleAvatars, (index) { - return CircleAvatar( - radius: avatarSize / 2, - backgroundColor: - AppConfig.gold.withAlpha(80), - child: const Icon( - Icons.question_mark, - size: 20.0, - ), - ); - }), - const Icon( - Icons.more_horiz, - size: 24.0, - ), - ], - ); - }, - ), - Text( - L10n.of(context).courseStartDesc, - style: theme.textTheme.titleMedium, - ), + ); + }), + const Icon( + Icons.more_horiz, + size: 24.0, + ), + ], + ); + }, + ), + Text( + L10n.of(context).courseStartDesc, + style: theme.textTheme.titleMedium, + ), + ], + ), + ), + Column( + spacing: 24.0, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () async { + final resp = await showFutureLoadingDialog( + context: context, + future: getSpaceId, + ); + if (mounted && !resp.isError) { + context.go("/rooms/spaces/${resp.result}/invite"); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.onPrimaryContainer, + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.upload_file), + Text(L10n.of(context).inviteYourFriends), ], ), ), - Column( - spacing: 24.0, - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - onPressed: () async { - final resp = await showFutureLoadingDialog( - context: context, - future: getSpaceId, - ); - if (mounted && !resp.isError) { - context.go("/rooms/spaces/${resp.result}/invite"); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primaryContainer, - foregroundColor: - theme.colorScheme.onPrimaryContainer, - ), - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.upload_file), - Text(L10n.of(context).inviteYourFriends), - ], - ), - ), - ElevatedButton( - onPressed: () async { - final resp = await showFutureLoadingDialog( - context: context, - future: getSpaceId, - ); - if (mounted && !resp.isError) { - context.go("/rooms/spaces/${resp.result}"); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primaryContainer, - foregroundColor: - theme.colorScheme.onPrimaryContainer, - ), - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(L10n.of(context).playWithAI), - ], - ), - ), - ], + ElevatedButton( + onPressed: () async { + final resp = await showFutureLoadingDialog( + context: context, + future: getSpaceId, + ); + if (mounted && !resp.isError) { + context.go("/rooms/spaces/${resp.result}"); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.onPrimaryContainer, + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).playWithAI), + ], + ), ), ], ), - ), + ], ), ), - ); - }, + ), + ), ); } } diff --git a/lib/pangea/course_creation/course_plan_tile_widget.dart b/lib/pangea/course_creation/course_plan_tile_widget.dart index d9130bc49..5205b9a46 100644 --- a/lib/pangea/course_creation/course_plan_tile_widget.dart +++ b/lib/pangea/course_creation/course_plan_tile_widget.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; import 'package:fluffychat/pangea/course_plans/map_clipper.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; class CoursePlanTile extends StatelessWidget { + final String courseId; final CoursePlanModel course; final VoidCallback onTap; @@ -18,6 +19,7 @@ class CoursePlanTile extends StatelessWidget { const CoursePlanTile({ super.key, + required this.courseId, required this.course, required this.onTap, this.titleFontSize, @@ -72,7 +74,7 @@ class CoursePlanTile extends StatelessWidget { ), ), CourseInfoChips( - course, + courseId, padding: const EdgeInsets.symmetric( horizontal: 4.0, vertical: 2.0, diff --git a/lib/pangea/course_creation/course_search_provider.dart b/lib/pangea/course_creation/course_search_provider.dart index aa5f9c185..c2c9636b7 100644 --- a/lib/pangea/course_creation/course_search_provider.dart +++ b/lib/pangea/course_creation/course_search_provider.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_filter.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plans_repo.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; mixin CourseSearchProvider on State { bool loading = true; Object? error; - List courses = []; + Map courses = {}; LanguageModel? targetLanguageFilter; @override @@ -36,7 +37,8 @@ mixin CourseSearchProvider on State { loading = true; error = null; }); - courses = await CoursePlansRepo.searchByFilter(filter: _filter); + final resp = await CoursePlansRepo.searchByFilter(filter: _filter); + courses = resp.coursePlans; } catch (e, s) { debugPrint("Failed to load courses: $e\n$s"); error = e; diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 38b6cbf68..a8cf73a9d 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -8,8 +8,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_view.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -39,7 +40,22 @@ class SelectedCourse extends StatefulWidget { SelectedCourseController createState() => SelectedCourseController(); } -class SelectedCourseController extends State { +class SelectedCourseController extends State + with CoursePlanProvider { + @override + initState() { + super.initState(); + loadCourse(widget.courseId).then((_) => loadTopics()); + } + + @override + void didUpdateWidget(covariant SelectedCourse oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.courseId != widget.courseId) { + loadCourse(widget.courseId).then((_) => loadTopics()); + } + } + String get title { switch (widget.mode) { case SelectedCourseMode.launch: @@ -65,15 +81,18 @@ class SelectedCourseController extends State { Future submit(CoursePlanModel course) async { switch (widget.mode) { case SelectedCourseMode.launch: - return launchCourse(course); + return launchCourse(widget.courseId, course); case SelectedCourseMode.addToSpace: return addCourseToSpace(course); case SelectedCourseMode.join: - return joinCourse(course); + return joinCourse(); } } - Future launchCourse(CoursePlanModel course) async { + Future launchCourse( + String courseId, + CoursePlanModel course, + ) async { final client = Matrix.of(context).client; final Completer completer = Completer(); client @@ -88,7 +107,7 @@ class SelectedCourseController extends State { sdk.StateEvent( type: PangeaEventTypes.coursePlan, content: { - "uuid": course.uuid, + "uuid": courseId, }, ), ], @@ -129,7 +148,7 @@ class SelectedCourseController extends State { context.go("/rooms/spaces/${space.id}/details"); } - Future joinCourse(CoursePlanModel course) async { + Future joinCourse() async { if (widget.roomChunk == null) { throw Exception("Room chunk is null"); } diff --git a/lib/pangea/course_creation/selected_course_view.dart b/lib/pangea/course_creation/selected_course_view.dart index 0bbb23964..0027c3663 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_page.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; import 'package:fluffychat/pangea/course_plans/map_clipper.dart'; import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -29,6 +27,7 @@ class SelectedCourseView extends StatelessWidget { const double mediumIconSize = 16.0; const double smallIconSize = 12.0; + final course = controller.course; return Scaffold( appBar: AppBar( title: Text( @@ -36,17 +35,18 @@ class SelectedCourseView extends StatelessWidget { ), ), body: SafeArea( - child: CoursePlanBuilder( - courseId: controller.widget.courseId, - onNotFound: () => context.go("/rooms/course/own"), - builder: (context, courseController) { - final course = courseController.course; - return Container( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500.0), - child: course == null - ? const Center(child: CircularProgressIndicator.adaptive()) + child: Container( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500.0), + child: controller.loadingCourse + ? const Center(child: CircularProgressIndicator.adaptive()) + : controller.courseError != null || course == null + ? Center( + child: ErrorIndicator( + message: L10n.of(context).oopsSomethingWentWrong, + ), + ) : Column( children: [ Expanded( @@ -57,7 +57,7 @@ class SelectedCourseView extends StatelessWidget { right: 12.0, ), child: ListView.builder( - itemCount: course.loadedTopics.length + 2, + itemCount: course.topicIds.length + 2, itemBuilder: (context, index) { String displayname = course.title; final roomChunk = controller.widget.roomChunk; @@ -104,7 +104,7 @@ class SelectedCourseView extends StatelessWidget { ), ), CourseInfoChips( - course, + controller.widget.courseId, fontSize: descFontSize, iconSize: smallIconSize, ), @@ -135,11 +135,17 @@ class SelectedCourseView extends StatelessWidget { index--; - if (index >= course.loadedTopics.length) { + if (index >= course.topicIds.length) { return const SizedBox(height: 12.0); } - final topic = course.loadedTopics[index]; + final topicId = course.topicIds[index]; + final topic = course.loadedTopics[topicId]; + + if (topic == null) { + return const SizedBox(); + } + return Padding( padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -201,17 +207,6 @@ class SelectedCourseView extends StatelessWidget { fontSize: descFontSize, iconSize: smallIconSize, ), - CourseInfoChip( - icon: Icons - .event_note_outlined, - text: L10n.of(context) - .numActivityPlans( - topic.loadedActivities - .length, - ), - fontSize: descFontSize, - iconSize: smallIconSize, - ), ], ), ), @@ -307,9 +302,7 @@ class SelectedCourseView extends StatelessWidget { ), ], ), - ), - ); - }, + ), ), ), ); diff --git a/lib/pangea/course_plans/activity_summaries_provider.dart b/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart similarity index 66% rename from lib/pangea/course_plans/activity_summaries_provider.dart rename to lib/pangea/course_plans/course_activities/activity_summaries_provider.dart index 699919d1a..0fdef8f17 100644 --- a/lib/pangea/course_plans/activity_summaries_provider.dart +++ b/lib/pangea/course_plans/course_activities/activity_summaries_provider.dart @@ -4,8 +4,8 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; mixin ActivitySummariesProvider on State { @@ -56,6 +56,7 @@ mixin ActivitySummariesProvider on State { ), ) .map((e) => e.activityPlan.activityId) + .whereType() .toSet(); } @@ -70,20 +71,11 @@ mixin ActivitySummariesProvider on State { bool _hasCompletedTopic( String userID, CourseTopicModel topic, - CoursePlanModel course, ) { - final topicIndex = course.loadedTopics.indexWhere( - (t) => t.uuid == topic.uuid, - ); - - if (topicIndex == -1) { - throw Exception('Topic not found'); - } - - final topicActivities = course.loadedTopics[topicIndex].loadedActivities; - final topicActivityIds = topicActivities.map((a) => a.activityId).toSet(); - final numTwoPersonActivities = - topicActivities.where((a) => a.req.numberOfParticipants <= 2).length; + final topicActivityIds = topic.activityIds.toSet(); + final numTwoPersonActivities = topic.loadedActivities.values + .where((a) => a.req.numberOfParticipants <= 2) + .length; final completedTopicActivities = _completedActivities(userID).intersection(topicActivityIds); @@ -91,36 +83,40 @@ mixin ActivitySummariesProvider on State { return completedTopicActivities.length >= numTwoPersonActivities; } - int currentTopicIndex( + String? currentTopicId( String userID, CoursePlanModel course, ) { - if (course.loadedTopics.isEmpty) return -1; - for (int i = 0; i < course.loadedTopics.length; i++) { - if (!_hasCompletedTopic(userID, course.loadedTopics[i], course)) { - return i; + if (course.loadedTopics.isEmpty) { + return null; + } + + for (int i = 0; i < course.topicIds.length; i++) { + final topicId = course.topicIds[i]; + final topic = course.loadedTopics[topicId]; + if (topic == null) continue; + if (!topic.activityListComplete) { + return null; + } + + if (!_hasCompletedTopic(userID, topic) && topic.activityIds.isNotEmpty) { + return topicId; } } - return 0; + return course.topicIds.last; } - Future>> topicsToUsers( + Map> topicsToUsers( Room room, CoursePlanModel course, - ) async { + ) { final Map> topicUserMap = {}; - final users = await room.requestParticipants( - [Membership.join, Membership.invite, Membership.knock], - false, - true, - ); - + final users = room.getParticipants(); for (final user in users) { if (user.id == BotName.byEnvironment) continue; - final topicIndex = currentTopicIndex(user.id, course); - if (topicIndex != -1) { - final topicID = course.loadedTopics[topicIndex].uuid; - topicUserMap.putIfAbsent(topicID, () => []).add(user); + final topicId = currentTopicId(user.id, course); + if (topicId != null) { + topicUserMap.putIfAbsent(topicId, () => []).add(user); } } return topicUserMap; diff --git a/lib/pangea/course_plans/course_activities/course_activity_repo.dart b/lib/pangea/course_plans/course_activities/course_activity_repo.dart new file mode 100644 index 000000000..a644afd4f --- /dev/null +++ b/lib/pangea/course_plans/course_activities/course_activity_repo.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_response.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseActivityRepo { + static final Map> _cache = {}; + static final GetStorage _storage = GetStorage('course_activity_storage'); + + static Future get( + TranslateActivityRequest request, + String batchId, + ) async { + await _storage.initStorage; + final activities = getCached(request).plans; + + final toFetch = + request.activityIds.where((id) => !activities.containsKey(id)).toList(); + + if (toFetch.isNotEmpty) { + final fetchedActivities = await _fetch(request, batchId); + activities.addAll(fetchedActivities.plans); + await _setCached(fetchedActivities, request.l1); + } + + return TranslateActivityResponse(plans: activities); + } + + static Future translate( + TranslateActivityRequest request, + ) async { + final Requests req = Requests( + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.getLocalizedActivity, + body: request.toJson(), + ); + + if (res.statusCode != 200) { + throw Exception( + "Failed to translate activity: ${res.statusCode} ${res.body}", + ); + } + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + + final response = TranslateActivityResponse.fromJson(decodedBody); + + return response; + } + + static Future _fetch( + TranslateActivityRequest request, + String batchId, + ) async { + if (_cache.containsKey(batchId)) { + return _cache[batchId]!.future; + } + + final completer = Completer(); + _cache[batchId] = completer; + + try { + final response = await translate(request); + completer.complete(response); + return response; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + _cache.remove(batchId); + } + } + + static TranslateActivityResponse getCached( + TranslateActivityRequest request, + ) { + final Map activities = {}; + for (final id in request.activityIds) { + final cacheKey = "${id}_${request.l1}"; + final sentActivityFeedback = sentFeedback[cacheKey]; + if (sentActivityFeedback != null && + DateTime.now().difference(sentActivityFeedback) > + const Duration(minutes: 15)) { + _storage.remove(cacheKey); + _clearSentFeedback(cacheKey, request.l1); + continue; + } + + final json = _storage.read>(cacheKey); + if (json != null) { + try { + final activity = ActivityPlanModel.fromJson(json); + activities[id] = activity; + } catch (e) { + // ignore invalid cached data + _storage.remove(cacheKey); + } + } + } + + return TranslateActivityResponse(plans: activities); + } + + static Future _setCached( + TranslateActivityResponse activities, + String l1, + ) async { + final List futures = []; + for (final entry in activities.plans.entries) { + final cacheKey = "${entry.key}_$l1"; + futures.add(_storage.write(cacheKey, entry.value.toJson())); + } + await Future.wait(futures); + } + + static Future clearCache() async { + await _storage.erase(); + } + + static Map get sentFeedback { + final entry = _storage.read("sent_feedback"); + if (entry != null && entry is Map) { + try { + return Map.from( + entry.map((key, value) => MapEntry(key, DateTime.parse(value))), + ); + } catch (e) { + _storage.remove("sent_feedback"); + } + } + return {}; + } + + static Future setSentFeedback( + String activityId, + String l1, + ) async { + final currentValue = sentFeedback; + currentValue["${activityId}_$l1"] = DateTime.now(); + await _storage.write( + "sent_feedback", + currentValue.map((key, value) => MapEntry(key, value.toIso8601String())), + ); + } + + static Future _clearSentFeedback( + String activityId, + String l1, + ) async { + final currentValue = sentFeedback; + currentValue.remove("${activityId}_$l1"); + await _storage.write( + "sent_feedback", + currentValue.map((key, value) => MapEntry(key, value.toIso8601String())), + ); + } +} diff --git a/lib/pangea/course_plans/course_activities/course_activity_translation_request.dart b/lib/pangea/course_plans/course_activities/course_activity_translation_request.dart new file mode 100644 index 000000000..0f6cf2cf2 --- /dev/null +++ b/lib/pangea/course_plans/course_activities/course_activity_translation_request.dart @@ -0,0 +1,23 @@ +class TranslateActivityRequest { + List activityIds; + String l1; + + TranslateActivityRequest({ + required this.activityIds, + required this.l1, + }); + + Map toJson() => { + "activity_ids": activityIds, + "l1": l1, + }; + + factory TranslateActivityRequest.fromJson(Map json) { + return TranslateActivityRequest( + activityIds: json['activity_ids'] != null + ? List.from(json['activity_ids']) + : [], + l1: json['l1'], + ); + } +} diff --git a/lib/pangea/course_plans/course_activities/course_activity_translation_response.dart b/lib/pangea/course_plans/course_activities/course_activity_translation_response.dart new file mode 100644 index 000000000..ac1e995aa --- /dev/null +++ b/lib/pangea/course_plans/course_activities/course_activity_translation_response.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; + +class TranslateActivityResponse { + final Map plans; + + TranslateActivityResponse({required this.plans}); + + factory TranslateActivityResponse.fromJson(Map json) { + final plansEntry = json['plans'] as Map; + return TranslateActivityResponse( + plans: plansEntry.map( + (key, value) { + return MapEntry( + key, + ActivityPlanModel.fromJson(value), + ); + }, + ), + ); + } + + Map toJson() => { + "plans": plans.map((key, value) => MapEntry(key, value.toJson())), + }; +} diff --git a/lib/pangea/course_plans/course_activity_repo.dart b/lib/pangea/course_plans/course_activity_repo.dart deleted file mode 100644 index a4ca2e1b9..000000000 --- a/lib/pangea/course_plans/course_activity_repo.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseActivityRepo { - static final Map>> _cache = {}; - static final GetStorage _storage = GetStorage('course_activity_storage'); - - static Map get sentFeedback { - final entry = _storage.read("sent_feedback"); - if (entry != null && entry is Map) { - try { - return Map.from( - entry.map((key, value) => MapEntry(key, DateTime.parse(value))), - ); - } catch (e) { - _storage.remove("sent_feedback"); - } - } - return {}; - } - - static Future setSentFeedback(String activityId) async { - final currentValue = sentFeedback; - currentValue[activityId] = DateTime.now(); - await _storage.write( - "sent_feedback", - currentValue.map((key, value) => MapEntry(key, value.toIso8601String())), - ); - } - - static Future _clearSentFeedback(String activityId) async { - final currentValue = sentFeedback; - currentValue.remove(activityId); - await _storage.write( - "sent_feedback", - currentValue.map((key, value) => MapEntry(key, value.toIso8601String())), - ); - } - - static ActivityPlanModel? _getCached(String uuid) { - final sentActivityFeedback = sentFeedback[uuid]; - if (sentActivityFeedback != null && - DateTime.now().difference(sentActivityFeedback) > - const Duration(minutes: 15)) { - _storage.remove(uuid); - _clearSentFeedback(uuid); - return null; - } - - final json = _storage.read>(uuid); - if (json != null) { - try { - return ActivityPlanModel.fromJson(json); - } catch (e) { - // ignore invalid cached data - _storage.remove(uuid); - } - } - return null; - } - - static Future _setCached(ActivityPlanModel activity) => - _storage.write(activity.activityId, activity.toJson()); - - static List getSync(List uuids) { - return uuids - .map((uuid) => _getCached(uuid)) - .whereType() - .toList(); - } - - static Future> get( - String topicId, - List uuids, - ) async { - final activities = []; - final toFetch = []; - - await _storage.initStorage; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - activities.add(cached); - } else { - toFetch.add(uuid); - } - } - - if (toFetch.isNotEmpty) { - final fetchedActivities = await _fetch(topicId, toFetch); - activities.addAll(fetchedActivities); - for (final activity in fetchedActivities) { - await _setCached(activity); - } - } - - return activities; - } - - static Future> _fetch( - String topicId, - List uuids, - ) async { - if (_cache.containsKey(topicId)) { - return _cache[topicId]!.future; - } - - final completer = Completer>(); - _cache[topicId] = completer; - - final where = { - "id": {"in": uuids.join(",")}, - }; - final limit = uuids.length; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - - final cmsCoursePlanActivitiesResult = await payload.find( - CmsCoursePlanActivity.slug, - CmsCoursePlanActivity.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - final imageUrls = await _fetchImageUrls( - cmsCoursePlanActivitiesResult.docs, - ); - - final activities = cmsCoursePlanActivitiesResult.docs - .map((a) => a.toActivityPlanModel(imageUrls[a.id])) - .toList(); - - completer.complete(activities); - return activities; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - _cache.remove(topicId); - } - } - - static Future> _fetchImageUrls( - List activities, - ) async { - // map of mediaId to activityId - final activityToMediaId = Map.fromEntries( - activities - .where((a) => a.coursePlanActivityMedia?.docs?.isNotEmpty ?? false) - .map((a) { - final mediaIds = a.coursePlanActivityMedia?.docs; - return MapEntry(mediaIds?.firstOrNull, a.id); - }), - ); - - final mediaIds = activityToMediaId.keys.whereType().toList(); - if (mediaIds.isEmpty) { - return {}; - } - - final where = { - "id": {"in": mediaIds.join(",")}, - }; - final limit = mediaIds.length; - - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCoursePlanActivityMediasResult = await payload.find( - CmsCoursePlanActivityMedia.slug, - CmsCoursePlanActivityMedia.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - return Map.fromEntries( - cmsCoursePlanActivityMediasResult.docs.map((media) { - final activityId = activityToMediaId[media.id]; - if (activityId != null && media.url != null) { - return MapEntry(activityId, '${Environment.cmsApi}${media.url!}'); - } - return null; - }).whereType>(), - ); - } - - static Future clearCache() async { - await _storage.erase(); - } -} diff --git a/lib/pangea/course_plans/course_info_batch_request.dart b/lib/pangea/course_plans/course_info_batch_request.dart new file mode 100644 index 000000000..9aecf6c4e --- /dev/null +++ b/lib/pangea/course_plans/course_info_batch_request.dart @@ -0,0 +1,9 @@ +class CourseInfoBatchRequest { + final String batchId; + final List uuids; + + CourseInfoBatchRequest({ + required this.batchId, + required this.uuids, + }); +} diff --git a/lib/pangea/course_plans/course_location_media_repo.dart b/lib/pangea/course_plans/course_location_media_repo.dart deleted file mode 100644 index 502e84d77..000000000 --- a/lib/pangea/course_plans/course_location_media_repo.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location_media.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseLocationMediaRepo { - static final Map>> _cache = {}; - static final GetStorage _storage = - GetStorage('course_location_media_storage'); - - static String? _getCached(String uuid) { - try { - return _storage.read(uuid) as String?; - } catch (e) { - // If parsing fails, remove the corrupted cache entry - _storage.remove(uuid); - } - return null; - } - - static Future _setCached(String uuid, String url) => - _storage.write(uuid, url); - - static List getSync(List uuids) { - final urls = []; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - urls.add(cached); - } - } - - return urls; - } - - static Future> get(String topicId, List uuids) async { - final urls = []; - final toFetch = []; - - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - urls.add(cached); - } else { - toFetch.add(uuid); - } - } - - if (toFetch.isNotEmpty) { - final fetched = await _fetch(topicId, toFetch); - urls.addAll(fetched.values); - for (final entry in fetched.entries) { - await _setCached(entry.key, entry.value); - } - } - - return urls; - } - - static Future> _fetch( - String topicId, - List uuids, - ) async { - if (_cache.containsKey(topicId)) { - return _cache[topicId]!.future; - } - - final completer = Completer>(); - _cache[topicId] = completer; - - final where = { - "id": {"in": uuids.join(",")}, - }; - final limit = uuids.length; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCoursePlanTopicLocationMediasResult = await payload.find( - CmsCoursePlanTopicLocationMedia.slug, - CmsCoursePlanTopicLocationMedia.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - final media = Map.fromEntries( - cmsCoursePlanTopicLocationMediasResult.docs - .map((e) => MapEntry(e.id, e.url!)), - ); - completer.complete(media); - return media; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - _cache.remove(topicId); - } - } - - static Future clearCache() async { - await _storage.erase(); - } -} diff --git a/lib/pangea/course_plans/course_location_repo.dart b/lib/pangea/course_plans/course_location_repo.dart deleted file mode 100644 index 1c0f47075..000000000 --- a/lib/pangea/course_plans/course_location_repo.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_model.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseLocationRepo { - static final Map>> _cache = {}; - static final GetStorage _storage = GetStorage('course_location_storage'); - - static CourseLocationModel? _getCached(String uuid) { - final json = _storage.read(uuid); - if (json != null) { - try { - return CourseLocationModel.fromJson(Map.from(json)); - } catch (e) { - // If parsing fails, remove the corrupted cache entry - _storage.remove(uuid); - } - } - return null; - } - - static Future _setCached(String uuid, CourseLocationModel location) => - _storage.write(uuid, location.toJson()); - - static List getSync(List uuids) { - final locations = []; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - locations.add(cached); - } - } - - return locations; - } - - static Future> get( - String courseId, - List uuids, - ) async { - final locations = []; - final toFetch = []; - - await _storage.initStorage; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - locations.add(cached); - } else { - toFetch.add(uuid); - } - } - - if (toFetch.isNotEmpty) { - final fetchedLocations = await _fetch(courseId, toFetch); - locations.addAll(fetchedLocations); - for (int i = 0; i < fetchedLocations.length; i++) { - final location = fetchedLocations[i]; - await _setCached(location.uuid, location); - } - } - - return locations; - } - - static Future> _fetch( - String topicId, - List uuids, - ) async { - if (_cache.containsKey(topicId)) { - return _cache[topicId]!.future; - } - - final completer = Completer>(); - _cache[topicId] = completer; - - final where = { - "id": {"in": uuids.join(",")}, - }; - final limit = uuids.length; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCoursePlanTopicLocationsResult = await payload.find( - CmsCoursePlanTopicLocation.slug, - CmsCoursePlanTopicLocation.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - final locations = cmsCoursePlanTopicLocationsResult.docs - .map((location) => location.toCourseLocationModel()) - .toList(); - - completer.complete(locations); - return locations; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - _cache.remove(topicId); - } - } - - static Future clearCache() async { - await _storage.erase(); - } -} diff --git a/lib/pangea/course_plans/course_locations/course_location_media_repo.dart b/lib/pangea/course_plans/course_locations/course_location_media_repo.dart new file mode 100644 index 000000000..28c78e5dd --- /dev/null +++ b/lib/pangea/course_plans/course_locations/course_location_media_repo.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/course_plans/course_info_batch_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_media_response.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_info.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location_media.dart'; +import 'package:fluffychat/pangea/payload_client/payload_client.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseLocationMediaRepo { + static final Map> _cache = {}; + static final GetStorage _storage = + GetStorage('course_location_media_storage'); + + static Future get( + CourseInfoBatchRequest request, + ) async { + await _storage.initStorage; + final cached = getCached(request); + final urls = List.from(cached.mediaUrls); + final toFetch = request.uuids + .where((uuid) => !cached.mediaUrls.any((e) => e.uuid == uuid)) + .toList(); + + if (toFetch.isNotEmpty) { + final fetched = await _fetch( + CourseInfoBatchRequest( + batchId: request.batchId, + uuids: toFetch, + ), + ); + + urls.addAll(fetched.mediaUrls); + await _setCached(fetched); + } + + return CourseLocationMediaResponse(mediaUrls: urls); + } + + static Future _fetch( + CourseInfoBatchRequest request, + ) async { + if (_cache.containsKey(request.batchId)) { + return _cache[request.batchId]!.future; + } + + final completer = Completer(); + _cache[request.batchId] = completer; + + final where = { + "id": {"in": request.uuids.join(",")}, + }; + final limit = request.uuids.length; + + try { + final PayloadClient payload = PayloadClient( + baseUrl: Environment.cmsApi, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + final cmsCoursePlanTopicLocationMediasResult = await payload.find( + CmsCoursePlanTopicLocationMedia.slug, + CmsCoursePlanTopicLocationMedia.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + + final resp = CourseLocationMediaResponse.fromCmsResponse( + cmsCoursePlanTopicLocationMediasResult, + ); + completer.complete(resp); + return resp; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + _cache.remove(request.batchId); + } + } + + static CourseLocationMediaResponse getCached( + CourseInfoBatchRequest request, + ) { + final List urls = []; + for (final uuid in request.uuids) { + try { + final url = _storage.read(uuid) as String?; + if (url != null) { + urls.add( + CourseMediaInfo( + uuid: uuid, + url: url, + ), + ); + } + } catch (e) { + // If parsing fails, remove the corrupted cache entry + _storage.remove(uuid); + } + } + return CourseLocationMediaResponse(mediaUrls: urls); + } + + static Future _setCached(CourseLocationMediaResponse response) async { + final List futures = []; + for (final entry in response.mediaUrls) { + futures.add(_storage.write(entry.uuid, entry.url)); + } + await Future.wait(futures); + } + + static Future clearCache() async { + await _storage.erase(); + } +} diff --git a/lib/pangea/course_plans/course_locations/course_location_media_response.dart b/lib/pangea/course_plans/course_locations/course_location_media_response.dart new file mode 100644 index 000000000..2eff7b9d5 --- /dev/null +++ b/lib/pangea/course_plans/course_locations/course_location_media_response.dart @@ -0,0 +1,28 @@ +import 'package:fluffychat/pangea/course_plans/course_media/course_media_info.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location_media.dart'; +import 'package:fluffychat/pangea/payload_client/paginated_response.dart'; + +class CourseLocationMediaResponse { + final List mediaUrls; + + CourseLocationMediaResponse({ + required this.mediaUrls, + }); + + factory CourseLocationMediaResponse.fromCmsResponse( + PayloadPaginatedResponse + cmsCoursePlanTopicLocationMediasResult, + ) { + return CourseLocationMediaResponse( + mediaUrls: cmsCoursePlanTopicLocationMediasResult.docs + .where((e) => e.url != null) + .map( + (e) => CourseMediaInfo( + uuid: e.id, + url: e.url!, + ), + ) + .toList(), + ); + } +} diff --git a/lib/pangea/course_plans/course_location_model.dart b/lib/pangea/course_plans/course_locations/course_location_model.dart similarity index 100% rename from lib/pangea/course_plans/course_location_model.dart rename to lib/pangea/course_plans/course_locations/course_location_model.dart diff --git a/lib/pangea/course_plans/course_locations/course_location_repo.dart b/lib/pangea/course_plans/course_locations/course_location_repo.dart new file mode 100644 index 000000000..6931ddff1 --- /dev/null +++ b/lib/pangea/course_plans/course_locations/course_location_repo.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/course_plans/course_info_batch_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_response.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart'; +import 'package:fluffychat/pangea/payload_client/payload_client.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseLocationRepo { + static final Map> _cache = {}; + static final GetStorage _storage = GetStorage('course_location_storage'); + + static Future get( + CourseInfoBatchRequest request, + ) async { + await _storage.initStorage; + final locations = getCached(request).locations; + + final toFetch = request.uuids + .where( + (id) => locations.indexWhere((location) => location.uuid == id) == -1, + ) + .toList(); + + if (toFetch.isNotEmpty) { + final fetchedLocations = await _fetch( + CourseInfoBatchRequest( + batchId: request.batchId, + uuids: toFetch, + ), + ); + locations.addAll(fetchedLocations.locations); + await _setCached(fetchedLocations); + } + + return CourseLocationResponse(locations: locations); + } + + static Future _fetch( + CourseInfoBatchRequest request, + ) async { + if (_cache.containsKey(request.batchId)) { + return _cache[request.batchId]!.future; + } + + final completer = Completer(); + _cache[request.batchId] = completer; + + final where = { + "id": {"in": request.uuids.join(",")}, + }; + final limit = request.uuids.length; + + try { + final PayloadClient payload = PayloadClient( + baseUrl: Environment.cmsApi, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + final cmsCoursePlanTopicLocationsResult = await payload.find( + CmsCoursePlanTopicLocation.slug, + CmsCoursePlanTopicLocation.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + + final response = CourseLocationResponse.fromCmsResponse( + cmsCoursePlanTopicLocationsResult, + ); + + completer.complete(response); + return response; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + _cache.remove(request.batchId); + } + } + + static CourseLocationResponse getCached( + CourseInfoBatchRequest request, + ) { + final List locations = []; + for (final uuid in request.uuids) { + final json = _storage.read(uuid); + if (json != null) { + try { + final location = + CourseLocationModel.fromJson(Map.from(json)); + locations.add(location); + } catch (e) { + // If parsing fails, remove the corrupted cache entry + _storage.remove(uuid); + } + } + } + + return CourseLocationResponse(locations: locations); + } + + static Future _setCached(CourseLocationResponse locations) { + final List futures = []; + for (final location in locations.locations) { + futures.add(_storage.write(location.uuid, location.toJson())); + } + return Future.wait(futures); + } + + static Future clearCache() async { + await _storage.erase(); + } +} diff --git a/lib/pangea/course_plans/course_locations/course_location_response.dart b/lib/pangea/course_plans/course_locations/course_location_response.dart new file mode 100644 index 000000000..ba177d8ed --- /dev/null +++ b/lib/pangea/course_plans/course_locations/course_location_response.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_model.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart'; +import 'package:fluffychat/pangea/payload_client/paginated_response.dart'; + +class CourseLocationResponse { + final List locations; + + CourseLocationResponse({ + required this.locations, + }); + + factory CourseLocationResponse.fromCmsResponse( + PayloadPaginatedResponse response, + ) { + final locations = response.docs + .map((location) => location.toCourseLocationModel()) + .toList(); + + return CourseLocationResponse(locations: locations); + } +} diff --git a/lib/pangea/course_plans/course_media/course_media_info.dart b/lib/pangea/course_plans/course_media/course_media_info.dart new file mode 100644 index 000000000..7556e66b4 --- /dev/null +++ b/lib/pangea/course_plans/course_media/course_media_info.dart @@ -0,0 +1,9 @@ +class CourseMediaInfo { + final String uuid; + final String url; + + CourseMediaInfo({ + required this.uuid, + required this.url, + }); +} diff --git a/lib/pangea/course_plans/course_media/course_media_repo.dart b/lib/pangea/course_plans/course_media/course_media_repo.dart new file mode 100644 index 000000000..a2d84546f --- /dev/null +++ b/lib/pangea/course_plans/course_media/course_media_repo.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/course_plans/course_info_batch_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_info.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_response.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart'; +import 'package:fluffychat/pangea/payload_client/payload_client.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseMediaRepo { + static final Map> _cache = {}; + static final GetStorage _storage = GetStorage('course_media_storage'); + + static Future get( + CourseInfoBatchRequest request, + ) async { + final urls = []; + + await _storage.initStorage; + urls.addAll(getCached(request).mediaUrls); + + final toFetch = request.uuids + .where((uuid) => !urls.any((e) => e.uuid == uuid)) + .toList(); + + if (toFetch.isNotEmpty) { + final fetchedUrls = await _fetch( + CourseInfoBatchRequest( + batchId: request.batchId, + uuids: toFetch, + ), + ); + urls.addAll(fetchedUrls.mediaUrls); + await _setCached(fetchedUrls); + } + + return CourseMediaResponse(mediaUrls: urls); + } + + static Future _fetch( + CourseInfoBatchRequest request, + ) async { + if (_cache.containsKey(request.batchId)) { + return _cache[request.batchId]!.future; + } + + final completer = Completer(); + _cache[request.batchId] = completer; + + final where = { + "id": {"in": request.uuids.join(",")}, + }; + final limit = request.uuids.length; + + try { + final PayloadClient payload = PayloadClient( + baseUrl: Environment.cmsApi, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + final cmsCoursePlanMediaResult = await payload.find( + CmsCoursePlanMedia.slug, + CmsCoursePlanMedia.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + + if (cmsCoursePlanMediaResult.docs.isEmpty) { + return CourseMediaResponse(mediaUrls: []); + } + + final response = CourseMediaResponse.fromCmsResponse( + cmsCoursePlanMediaResult, + ); + + completer.complete(response); + return response; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + _cache.remove(request.batchId); + } + } + + static CourseMediaResponse getCached(CourseInfoBatchRequest request) { + final urls = []; + + for (final uuid in request.uuids) { + final cached = _storage.read(uuid); + if (cached != null && cached is String) { + urls.add( + CourseMediaInfo( + uuid: uuid, + url: cached, + ), + ); + } + } + + return CourseMediaResponse(mediaUrls: urls); + } + + static Future _setCached(CourseMediaResponse response) async { + final List futures = []; + for (final entry in response.mediaUrls) { + futures.add(_storage.write(entry.uuid, entry.url)); + } + await Future.wait(futures); + } + + static Future clearCache() async { + await _storage.erase(); + } +} diff --git a/lib/pangea/course_plans/course_media/course_media_response.dart b/lib/pangea/course_plans/course_media/course_media_response.dart new file mode 100644 index 000000000..ed595f389 --- /dev/null +++ b/lib/pangea/course_plans/course_media/course_media_response.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/pangea/course_plans/course_media/course_media_info.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart'; +import 'package:fluffychat/pangea/payload_client/paginated_response.dart'; + +class CourseMediaResponse { + final List mediaUrls; + + CourseMediaResponse({ + required this.mediaUrls, + }); + + factory CourseMediaResponse.fromCmsResponse( + PayloadPaginatedResponse response, + ) { + return CourseMediaResponse( + mediaUrls: response.docs + .where((e) => e.url != null) + .map( + (e) => CourseMediaInfo( + uuid: e.id, + url: e.url!, + ), + ) + .toList(), + ); + } +} diff --git a/lib/pangea/course_plans/course_media_repo.dart b/lib/pangea/course_plans/course_media_repo.dart deleted file mode 100644 index 64bc0631c..000000000 --- a/lib/pangea/course_plans/course_media_repo.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseMediaRepo { - static final Map>> _cache = {}; - static final GetStorage _storage = GetStorage('course_media_storage'); - - static List getSync(List uuids) { - final urls = []; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - urls.add(cached); - } - } - - return urls; - } - - static Future> get(String courseId, List uuids) async { - final urls = []; - final toFetch = []; - - await _storage.initStorage; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - urls.add(cached); - } else { - toFetch.add(uuid); - } - } - - if (toFetch.isNotEmpty) { - final fetchedUrls = await _fetch(courseId, toFetch); - urls.addAll(fetchedUrls.values.toList()); - for (final entry in fetchedUrls.entries) { - await _setCached(entry.key, entry.value); - } - } - - return urls; - } - - static String? _getCached(String uuid) => _storage.read(uuid); - - static Future _setCached(String uuid, String url) => - _storage.write(uuid, url); - - static Future> _fetch( - String courseId, - List uuids, - ) async { - if (_cache.containsKey(courseId)) { - return _cache[courseId]!.future; - } - - final completer = Completer>(); - _cache[courseId] = completer; - - final where = { - "id": {"in": uuids.join(",")}, - }; - final limit = uuids.length; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCoursePlanMediaResult = await payload.find( - CmsCoursePlanMedia.slug, - CmsCoursePlanMedia.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - if (cmsCoursePlanMediaResult.docs.isEmpty) { - return {}; - } - - final media = Map.fromEntries( - cmsCoursePlanMediaResult.docs - .where((media) => media.url != null) - .map((media) => MapEntry(media.id, media.url!)), - ); - - completer.complete(media); - return media; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - _cache.remove(courseId); - } - } - - static Future clearCache() async { - await _storage.erase(); - } -} diff --git a/lib/pangea/course_plans/course_plan_builder.dart b/lib/pangea/course_plans/course_plan_builder.dart deleted file mode 100644 index 392b83685..000000000 --- a/lib/pangea/course_plans/course_plan_builder.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; - -class CoursePlanBuilder extends StatefulWidget { - final String? courseId; - final VoidCallback? onNotFound; - final Function(CoursePlanModel course)? onLoaded; - final Widget Function( - BuildContext context, - CoursePlanController controller, - ) builder; - - const CoursePlanBuilder({ - super.key, - required this.courseId, - required this.builder, - this.onNotFound, - this.onLoaded, - }); - - @override - State createState() => CoursePlanController(); -} - -class CoursePlanController extends State { - bool loading = true; - Object? error; - - CoursePlanModel? course; - - @override - void initState() { - super.initState(); - _initStorage().then((_) { - if (mounted) _loadCourse(); - }); - } - - @override - void didUpdateWidget(covariant CoursePlanBuilder oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.courseId != widget.courseId) { - _loadCourse(); - } - } - - Future _initStorage() async { - final futures = [ - GetStorage.init("course_storage"), - GetStorage.init("course_activity_storage"), - GetStorage.init("course_location_media_storage"), - GetStorage.init("course_location_storage"), - GetStorage.init("course_media_storage"), - GetStorage.init("course_topic_storage"), - ]; - - await Future.wait(futures); - } - - Future _loadCourse() async { - setState(() { - loading = true; - error = null; - course = null; - }); - - if (widget.courseId == null) { - widget.onNotFound?.call(); - setState(() => loading = false); - return; - } - - try { - course = await CoursePlansRepo.get(widget.courseId!); - widget.onLoaded?.call(course!); - } catch (e) { - widget.onNotFound?.call(); - error = e; - } finally { - if (mounted) setState(() => loading = false); - } - } - - @override - Widget build(BuildContext context) => widget.builder(context, this); -} diff --git a/lib/pangea/course_plans/course_plans_repo.dart b/lib/pangea/course_plans/course_plans_repo.dart deleted file mode 100644 index 4fd0989b8..000000000 --- a/lib/pangea/course_plans/course_plans_repo.dart +++ /dev/null @@ -1,257 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_media_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_media_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_repo.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseFilter { - final LanguageModel? targetLanguage; - final LanguageModel? languageOfInstructions; - final LanguageLevelTypeEnum? cefrLevel; - - CourseFilter({ - this.targetLanguage, - this.languageOfInstructions, - this.cefrLevel, - }); - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is CourseFilter && - other.targetLanguage == targetLanguage && - other.languageOfInstructions == languageOfInstructions && - other.cefrLevel == cefrLevel; - } - - @override - int get hashCode => - targetLanguage.hashCode ^ - languageOfInstructions.hashCode ^ - cefrLevel.hashCode; -} - -class CoursePlansRepo { - static final Map> cache = {}; - static final GetStorage _courseStorage = GetStorage("course_storage"); - static const Duration cacheDuration = Duration(days: 1); - - static DateTime? get lastUpdated { - final entry = _courseStorage.read("last_updated"); - if (entry != null && entry is String) { - try { - return DateTime.parse(entry); - } catch (e) { - _courseStorage.remove("last_updated"); - } - } - return null; - } - - static CoursePlanModel? _getCached(String id) { - if (lastUpdated != null && - DateTime.now().difference(lastUpdated!) > cacheDuration) { - clearCache(); - return null; - } - - final json = _courseStorage.read(id); - if (json != null) { - try { - return CoursePlanModel.fromJson(json); - } catch (e) { - _courseStorage.remove(id); - } - } - return null; - } - - static Future _setCached(CoursePlanModel coursePlan) async { - if (lastUpdated == null) { - await _courseStorage.write( - "last_updated", - DateTime.now().toIso8601String(), - ); - } - await _courseStorage.write(coursePlan.uuid, coursePlan.toJson()); - } - - static Future get(String id) async { - await _courseStorage.initStorage; - final cached = _getCached(id); - if (cached != null) { - return cached; - } - - if (cache.containsKey(id)) { - return cache[id]!.future; - } - - final completer = Completer(); - cache[id] = completer; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCoursePlan = await payload.findById( - "course-plans", - id, - CmsCoursePlan.fromJson, - ); - - final coursePlan = cmsCoursePlan.toCoursePlanModel(); - await _setCached(coursePlan); - await coursePlan.init(); - completer.complete(coursePlan); - return coursePlan; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - cache.remove(id); - } - } - - static Future> search( - List ids, { - Map? where, - }) async { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - - final missingIds = ids - .where( - (id) => _courseStorage.read(id) == null, - ) - .toList(); - - where ??= {}; - where["id"] = { - "in": missingIds, - }; - - final searchResult = await payload.find( - CmsCoursePlan.slug, - CmsCoursePlan.fromJson, - page: 1, - limit: 10, - where: where, - ); - - final coursePlans = searchResult.docs - .map( - (cmsCoursePlan) => cmsCoursePlan.toCoursePlanModel(), - ) - .toList(); - - for (final plan in coursePlans) { - await _setCached(plan); - } - - final futures = coursePlans.map((c) => c.init()); - await Future.wait(futures); - - return ids - .map((id) => _getCached(id)) - .whereType() - .toList(); - } - - static Future> searchByFilter({ - CourseFilter? filter, - }) async { - await _courseStorage.initStorage; - - final Map where = {}; - if (filter != null) { - int numberOfFilter = 0; - if (filter.cefrLevel != null) { - numberOfFilter += 1; - } - if (filter.languageOfInstructions != null) { - numberOfFilter += 1; - } - if (filter.targetLanguage != null) { - numberOfFilter += 1; - } - if (numberOfFilter > 1) { - where["and"] = []; - if (filter.cefrLevel != null) { - where["and"].add({ - "cefrLevel": {"equals": filter.cefrLevel!.string}, - }); - } - if (filter.languageOfInstructions != null) { - where["and"].add({ - "l1": { - "equals": filter.languageOfInstructions!.langCodeShort, - }, - }); - } - if (filter.targetLanguage != null) { - where["and"].add({ - "l2": {"equals": filter.targetLanguage!.langCodeShort}, - }); - } - } else if (numberOfFilter == 1) { - if (filter.cefrLevel != null) { - where["cefrLevel"] = {"equals": filter.cefrLevel!.string}; - } - if (filter.languageOfInstructions != null) { - where["l1"] = { - "equals": filter.languageOfInstructions!.langCode, - }; - } - if (filter.targetLanguage != null) { - where["l2"] = {"equals": filter.targetLanguage!.langCode}; - } - } - } - - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - - // Run the search for the given filter, selecting only the course IDs - final result = await payload.find( - CmsCoursePlan.slug, - (json) => json["id"] as String, - page: 1, - limit: 10, - where: where, - select: {"id": true}, - ); - - return search(result.docs, where: where); - } - - static Future clearCache() async { - final List futures = [ - CourseActivityRepo.clearCache(), - CourseLocationMediaRepo.clearCache(), - CourseLocationRepo.clearCache(), - CourseMediaRepo.clearCache(), - CourseTopicRepo.clearCache(), - _courseStorage.erase(), - ]; - - await Future.wait(futures); - } -} diff --git a/lib/pangea/course_plans/course_topic_model.dart b/lib/pangea/course_plans/course_topic_model.dart deleted file mode 100644 index 45c5de8c9..000000000 --- a/lib/pangea/course_plans/course_topic_model.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/course_plans/course_activity_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_media_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_location_repo.dart'; - -/// Represents a topic in the course planner response. -class CourseTopicModel { - final String title; - final String description; - final String uuid; - final List locationIds; - final List activityIds; - - CourseTopicModel({ - required this.title, - required this.description, - required this.uuid, - required this.activityIds, - required this.locationIds, - }); - - bool get locationListComplete => locationIds.length == loadedLocations.length; - List get loadedLocations => - CourseLocationRepo.getSync(locationIds); - Future> fetchLocations() => - CourseLocationRepo.get(uuid, locationIds); - String? get location => loadedLocations.firstOrNull?.name; - - bool get locationMediaListComplete => - loadedLocationMediaIds.length == - loadedLocations.map((e) => e.mediaIds.length).fold(0, (a, b) => a + b); - List get loadedLocationMediaIds => loadedLocations - .map((location) => CourseLocationMediaRepo.getSync(location.mediaIds)) - .expand((e) => e) - .toList(); - Future> fetchLocationMedia() async { - final allLocationMedia = []; - for (final location in await fetchLocations()) { - allLocationMedia.addAll( - await CourseLocationMediaRepo.get(uuid, location.mediaIds), - ); - } - return allLocationMedia; - } - - String? get imageUrl => loadedLocationMediaIds.isEmpty - ? null - : "${Environment.cmsApi}${loadedLocationMediaIds.first}"; - - bool get activityListComplete => - activityIds.length == loadedActivities.length; - List get loadedActivities => - CourseActivityRepo.getSync(activityIds); - Future> fetchActivities() => - CourseActivityRepo.get(uuid, activityIds); - - ActivityPlanModel? activityById(String activityId) => - loadedActivities.firstWhereOrNull( - (activity) => activity.activityId == activityId, - ); - - /// Deserialize from JSON - factory CourseTopicModel.fromJson(Map json) { - return CourseTopicModel( - title: json['title'] as String, - description: json['description'] as String, - uuid: json['uuid'] as String, - activityIds: (json['activity_ids'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - locationIds: (json['location_ids'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - ); - } - - /// Serialize to JSON - Map toJson() { - return { - 'title': title, - 'description': description, - 'uuid': uuid, - 'activity_ids': activityIds, - 'location_ids': locationIds, - }; - } -} diff --git a/lib/pangea/course_plans/course_topic_repo.dart b/lib/pangea/course_plans/course_topic_repo.dart deleted file mode 100644 index 33f6aa92d..000000000 --- a/lib/pangea/course_plans/course_topic_repo.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'dart:async'; - -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; -import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_topic.dart'; -import 'package:fluffychat/pangea/payload_client/payload_client.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class CourseTopicRepo { - static final Map>> _cache = {}; - static final GetStorage _storage = GetStorage('course_topic_storage'); - - static List getSync(List uuids) { - final topics = []; - - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - topics.add(cached); - } - } - - return topics; - } - - static Future> get( - String courseId, - List uuids, - ) async { - final topics = []; - final toFetch = []; - - await _storage.initStorage; - for (final uuid in uuids) { - final cached = _getCached(uuid); - if (cached != null) { - topics.add(cached); - } else { - toFetch.add(uuid); - } - } - - if (toFetch.isNotEmpty) { - final fetchedTopics = await _fetch(courseId, toFetch); - topics.addAll(fetchedTopics); - await _setCached(fetchedTopics); - } - - return topics; - } - - static CourseTopicModel? _getCached(String uuid) { - final json = _storage.read(uuid); - if (json != null) { - try { - return CourseTopicModel.fromJson(Map.from(json)); - } catch (e) { - _storage.remove(uuid); - } - } - return null; - } - - static Future _setCached(List topics) async { - for (final topic in topics) { - await _storage.write(topic.uuid, topic.toJson()); - } - } - - static Future> _fetch( - String courseId, - List uuids, - ) async { - if (_cache.containsKey(courseId)) { - return _cache[courseId]!.future; - } - - final completer = Completer>(); - _cache[courseId] = completer; - - final where = { - "id": {"in": uuids.join(",")}, - }; - - final limit = uuids.length; - - try { - final PayloadClient payload = PayloadClient( - baseUrl: Environment.cmsApi, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final cmsCourseTopicsResult = await payload.find( - CmsCoursePlanTopic.slug, - CmsCoursePlanTopic.fromJson, - where: where, - limit: limit, - page: 1, - sort: "createdAt", - ); - - final topics = cmsCourseTopicsResult.docs.map((topic) { - return topic.toCourseTopicModel(); - }).toList(); - - completer.complete(topics); - return topics; - } catch (e) { - completer.completeError(e); - rethrow; - } finally { - _cache.remove(courseId); - } - } - - static Future clearCache() async { - await _storage.erase(); - } -} diff --git a/lib/pangea/course_plans/course_topics/course_topic_model.dart b/lib/pangea/course_plans/course_topics/course_topic_model.dart new file mode 100644 index 000000000..bfb1915c3 --- /dev/null +++ b/lib/pangea/course_plans/course_topics/course_topic_model.dart @@ -0,0 +1,137 @@ +import 'package:collection/collection.dart'; + +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_info_batch_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_media_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_response.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +/// Represents a topic in the course planner response. +class CourseTopicModel { + final String title; + final String description; + final String uuid; + final List locationIds; + final List activityIds; + + CourseTopicModel({ + required this.title, + required this.description, + required this.uuid, + required this.activityIds, + required this.locationIds, + }); + + bool get locationListComplete => + locationIds.length == loadedLocations.locations.length; + + CourseLocationResponse get loadedLocations => CourseLocationRepo.getCached( + CourseInfoBatchRequest( + batchId: uuid, + uuids: locationIds, + ), + ); + Future fetchLocations() => CourseLocationRepo.get( + CourseInfoBatchRequest( + batchId: uuid, + uuids: locationIds, + ), + ); + + String? get location => loadedLocations.locations.firstOrNull?.name; + + bool get locationMediaListComplete => + loadedLocationMediaIds.length == + loadedLocations.locations + .map((e) => e.mediaIds.length) + .fold(0, (a, b) => a + b); + + List get loadedLocationMediaIds => loadedLocations.locations + .map( + (location) => CourseLocationMediaRepo.getCached( + CourseInfoBatchRequest( + batchId: uuid, + uuids: location.mediaIds, + ), + ).mediaUrls, + ) + .expand((e) => e) + .map((e) => e.url) + .toList(); + + Future> fetchLocationMedia() async { + final allLocationMedia = []; + final locationResp = await fetchLocations(); + for (final location in locationResp.locations) { + final mediaResp = await CourseLocationMediaRepo.get( + CourseInfoBatchRequest( + batchId: uuid, + uuids: location.mediaIds, + ), + ); + + allLocationMedia.addAll(mediaResp.mediaUrls.map((e) => e.url)); + } + return allLocationMedia; + } + + String? get imageUrl => loadedLocationMediaIds.isEmpty + ? null + : "${Environment.cmsApi}${loadedLocationMediaIds.first}"; + + bool get activityListComplete => + activityIds.length == loadedActivities.length; + + Map get loadedActivities => + CourseActivityRepo.getCached( + TranslateActivityRequest( + activityIds: activityIds, + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + ).plans; + + Future> fetchActivities() async { + final resp = await CourseActivityRepo.get( + TranslateActivityRequest( + activityIds: activityIds, + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + uuid, + ); + + return resp.plans; + } + + /// Deserialize from JSON + factory CourseTopicModel.fromJson(Map json) { + final List? activityIdsEntry = + json['activity_ids'] as List? ?? + json['activityIds'] as List?; + final List? locationIdsEntry = + json['location_ids'] as List? ?? + json['locationIds'] as List?; + + return CourseTopicModel( + title: json['title'] as String, + description: json['description'] as String, + uuid: json['uuid'] as String, + activityIds: activityIdsEntry?.map((e) => e as String).toList() ?? [], + locationIds: locationIdsEntry?.map((e) => e as String).toList() ?? [], + ); + } + + /// Serialize to JSON + Map toJson() { + return { + 'title': title, + 'description': description, + 'uuid': uuid, + 'activity_ids': activityIds, + 'location_ids': locationIds, + }; + } +} diff --git a/lib/pangea/course_plans/course_topics/course_topic_repo.dart b/lib/pangea/course_plans/course_topics/course_topic_repo.dart new file mode 100644 index 000000000..1b0a152bc --- /dev/null +++ b/lib/pangea/course_plans/course_topics/course_topic_repo.dart @@ -0,0 +1,135 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_translation_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_translation_response.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseTopicRepo { + static final Map> _cache = {}; + static final GetStorage _storage = GetStorage('course_topic_storage'); + + static Future get( + TranslateTopicRequest request, + String batchId, + ) async { + await _storage.initStorage; + final topics = getCached(request).topics; + + final toFetch = + request.topicIds.where((uuid) => !topics.containsKey(uuid)).toList(); + + if (toFetch.isNotEmpty) { + final fetchedTopics = await _fetch( + TranslateTopicRequest( + topicIds: toFetch, + l1: request.l1, + ), + batchId, + ); + topics.addAll(fetchedTopics.topics); + await _setCached( + fetchedTopics, + request.l1, + ); + } + + return TranslateTopicResponse(topics: topics); + } + + static Future translate( + TranslateTopicRequest request, + ) async { + final Requests req = Requests( + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.getLocalizedTopic, + body: request.toJson(), + ); + + if (res.statusCode != 200) { + throw Exception( + "Failed to translate topic. Status code: ${res.statusCode}", + ); + } + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + + final response = TranslateTopicResponse.fromJson(decodedBody); + + return response; + } + + static Future _fetch( + TranslateTopicRequest request, + String batchId, + ) async { + if (_cache.containsKey(batchId)) { + return _cache[batchId]!.future; + } + + final completer = Completer(); + _cache[batchId] = completer; + + try { + final response = await translate(request); + completer.complete(response); + return response; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + _cache.remove(batchId); + } + } + + static TranslateTopicResponse getCached( + TranslateTopicRequest request, + ) { + final Map topics = {}; + for (final uuid in request.topicIds) { + final cacheKey = "${uuid}_${request.l1}"; + final json = _storage.read(cacheKey); + if (json != null) { + try { + final topic = CourseTopicModel.fromJson( + Map.from(json), + ); + topics[uuid] = topic; + } catch (e) { + _storage.remove(cacheKey); + } + } + } + + return TranslateTopicResponse(topics: topics); + } + + static Future _setCached( + TranslateTopicResponse response, + String l1, + ) async { + final List futures = []; + for (final entry in response.topics.entries) { + futures.add( + _storage.write( + "${entry.key}_$l1", + entry.value.toJson(), + ), + ); + } + await Future.wait(futures); + } + + static Future clearCache() async { + await _storage.erase(); + } +} diff --git a/lib/pangea/course_plans/course_topics/course_topic_translation_request.dart b/lib/pangea/course_plans/course_topics/course_topic_translation_request.dart new file mode 100644 index 000000000..a2a7fd1ea --- /dev/null +++ b/lib/pangea/course_plans/course_topics/course_topic_translation_request.dart @@ -0,0 +1,22 @@ +class TranslateTopicRequest { + List topicIds; + String l1; + + TranslateTopicRequest({ + required this.topicIds, + required this.l1, + }); + + Map toJson() => { + "topic_ids": topicIds, + "l1": l1, + }; + + factory TranslateTopicRequest.fromJson(Map json) { + return TranslateTopicRequest( + topicIds: + json['topic_ids'] != null ? List.from(json['topic_ids']) : [], + l1: json['l1'], + ); + } +} diff --git a/lib/pangea/course_plans/course_topics/course_topic_translation_response.dart b/lib/pangea/course_plans/course_topics/course_topic_translation_response.dart new file mode 100644 index 000000000..68b7a3829 --- /dev/null +++ b/lib/pangea/course_plans/course_topics/course_topic_translation_response.dart @@ -0,0 +1,23 @@ +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart'; + +class TranslateTopicResponse { + final Map topics; + + TranslateTopicResponse({required this.topics}); + + factory TranslateTopicResponse.fromJson(Map json) { + final topicsEntry = json['topics'] as Map; + return TranslateTopicResponse( + topics: topicsEntry.map( + (key, value) => MapEntry( + key, + CourseTopicModel.fromJson(value), + ), + ), + ); + } + + Map toJson() => { + "topics": topics.map((key, value) => MapEntry(key, value.toJson())), + }; +} diff --git a/lib/pangea/course_plans/courses/course_filter.dart b/lib/pangea/course_plans/courses/course_filter.dart new file mode 100644 index 000000000..b4f4aeb1f --- /dev/null +++ b/lib/pangea/course_plans/courses/course_filter.dart @@ -0,0 +1,78 @@ +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + +class CourseFilter { + final LanguageModel? targetLanguage; + final LanguageModel? languageOfInstructions; + final LanguageLevelTypeEnum? cefrLevel; + + CourseFilter({ + this.targetLanguage, + this.languageOfInstructions, + this.cefrLevel, + }); + + Map get whereFilter { + final Map where = {}; + int numberOfFilter = 0; + if (cefrLevel != null) { + numberOfFilter += 1; + } + if (languageOfInstructions != null) { + numberOfFilter += 1; + } + if (targetLanguage != null) { + numberOfFilter += 1; + } + if (numberOfFilter > 1) { + where["and"] = []; + if (cefrLevel != null) { + where["and"].add({ + "cefrLevel": {"equals": cefrLevel!.string}, + }); + } + if (languageOfInstructions != null) { + where["and"].add({ + "l1": { + "equals": languageOfInstructions!.langCodeShort, + }, + }); + } + if (targetLanguage != null) { + where["and"].add({ + "l2": {"equals": targetLanguage!.langCodeShort}, + }); + } + } else if (numberOfFilter == 1) { + if (cefrLevel != null) { + where["cefrLevel"] = {"equals": cefrLevel!.string}; + } + if (languageOfInstructions != null) { + where["l1"] = { + "equals": languageOfInstructions!.langCode, + }; + } + if (targetLanguage != null) { + where["l2"] = {"equals": targetLanguage!.langCode}; + } + } + + return where; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CourseFilter && + other.targetLanguage == targetLanguage && + other.languageOfInstructions == languageOfInstructions && + other.cefrLevel == cefrLevel; + } + + @override + int get hashCode => + targetLanguage.hashCode ^ + languageOfInstructions.hashCode ^ + cefrLevel.hashCode; +} diff --git a/lib/pangea/course_plans/courses/course_plan_builder.dart b/lib/pangea/course_plans/courses/course_plan_builder.dart new file mode 100644 index 000000000..095bc4ff4 --- /dev/null +++ b/lib/pangea/course_plans/courses/course_plan_builder.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'course_plans_repo.dart'; + +mixin CoursePlanProvider on State { + bool loadingCourse = true; + Object? courseError; + + bool loadingTopics = false; + Object? topicError; + + Map activityErrors = {}; + + CoursePlanModel? course; + + Future _initStorage() async { + final futures = [ + GetStorage.init("course_storage"), + GetStorage.init("course_activity_storage"), + GetStorage.init("course_location_media_storage"), + GetStorage.init("course_location_storage"), + GetStorage.init("course_media_storage"), + GetStorage.init("course_topic_storage"), + ]; + + await Future.wait(futures); + } + + Future loadCourse(String courseId) async { + await _initStorage(); + setState(() { + loadingCourse = true; + courseError = null; + course = null; + }); + + try { + course = await CoursePlansRepo.get( + GetLocalizedCoursesRequest( + coursePlanIds: [courseId], + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + ); + } catch (e) { + courseError = e; + } finally { + if (mounted) setState(() => loadingCourse = false); + } + } + + Future loadTopics() async { + setState(() { + loadingTopics = true; + topicError = null; + }); + + try { + if (course == null) { + throw Exception("Course is null"); + } + + final courseFutures = [ + course!.fetchMediaUrls(), + course!.fetchTopics(), + ]; + await Future.wait(courseFutures); + } catch (e) { + topicError = e; + } finally { + if (mounted) setState(() => loadingTopics = false); + } + } + + Future loadActivity(String topicId) async { + setState(() { + activityErrors[topicId] = null; + }); + + try { + final topic = course?.loadedTopics[topicId]; + if (topic == null) { + throw Exception("Topic is null"); + } + + final topicFutures = []; + topicFutures.add(topic.fetchActivities()); + topicFutures.add(topic.fetchLocationMedia()); + await Future.wait(topicFutures); + } catch (e) { + activityErrors[topicId] = e; + } finally { + if (mounted) { + setState(() {}); + } + } + } +} diff --git a/lib/pangea/course_plans/course_plan_event.dart b/lib/pangea/course_plans/courses/course_plan_event.dart similarity index 100% rename from lib/pangea/course_plans/course_plan_event.dart rename to lib/pangea/course_plans/courses/course_plan_event.dart diff --git a/lib/pangea/course_plans/course_plan_model.dart b/lib/pangea/course_plans/courses/course_plan_model.dart similarity index 62% rename from lib/pangea/course_plans/course_plan_model.dart rename to lib/pangea/course_plans/courses/course_plan_model.dart index 2adb47d04..acd6ace79 100644 --- a/lib/pangea/course_plans/course_plan_model.dart +++ b/lib/pangea/course_plans/courses/course_plan_model.dart @@ -1,13 +1,16 @@ import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/course_plans/course_media_repo.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_topic_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_info_batch_request.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_response.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_translation_request.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/widgets/matrix.dart'; /// Represents a course plan in the course planner response. class CoursePlanModel { @@ -53,28 +56,6 @@ class CoursePlanModel { baseLanguageModel?.langCode.toUpperCase() ?? languageOfInstructions.toUpperCase(); - String? topicID(String activityID) { - for (final topic in loadedTopics) { - if (topic.activityIds.any((id) => id == activityID)) { - return topic.uuid; - } - } - return null; - } - - int get totalActivities => - loadedTopics.fold(0, (sum, topic) => sum + topic.activityIds.length); - - ActivityPlanModel? activityById(String activityID) { - for (final topic in loadedTopics) { - final activity = topic.activityById(activityID); - if (activity != null) { - return activity; - } - } - return null; - } - /// Deserialize from JSON factory CoursePlanModel.fromJson(Map json) { return CoursePlanModel( @@ -114,37 +95,42 @@ class CoursePlanModel { } bool get topicListComplete => topicIds.length == loadedTopics.length; - List get loadedTopics => CourseTopicRepo.getSync(topicIds); - Future> fetchTopics() => - CourseTopicRepo.get(uuid, topicIds); - bool get mediaListComplete => mediaIds.length == loadedMediaUrls.length; - List get loadedMediaUrls => CourseMediaRepo.getSync(mediaIds); - Future> fetchMediaUrls() => CourseMediaRepo.get(uuid, mediaIds); - String? get imageUrl => loadedMediaUrls.isEmpty - ? loadedTopics + Map get loadedTopics => CourseTopicRepo.getCached( + TranslateTopicRequest( + topicIds: topicIds, + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + ).topics; + + Future> fetchTopics() async { + final resp = await CourseTopicRepo.get( + TranslateTopicRequest( + topicIds: topicIds, + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + uuid, + ); + return resp.topics; + } + + bool get mediaListComplete => + mediaIds.length == loadedMediaUrls.mediaUrls.length; + CourseMediaResponse get loadedMediaUrls => CourseMediaRepo.getCached( + CourseInfoBatchRequest( + batchId: uuid, + uuids: mediaIds, + ), + ); + Future fetchMediaUrls() => CourseMediaRepo.get( + CourseInfoBatchRequest( + batchId: uuid, + uuids: mediaIds, + ), + ); + String? get imageUrl => loadedMediaUrls.mediaUrls.isEmpty + ? loadedTopics.values .lastWhereOrNull((topic) => topic.imageUrl != null) ?.imageUrl - : "${Environment.cmsApi}${loadedMediaUrls.first}"; - - Future init() async { - final courseFutures = [ - fetchMediaUrls(), - fetchTopics(), - ]; - await Future.wait(courseFutures); - - final topicFutures = []; - topicFutures.addAll( - loadedTopics.map( - (topic) => topic.fetchActivities(), - ), - ); - topicFutures.addAll( - loadedTopics.map( - (topic) => topic.fetchLocationMedia(), - ), - ); - await Future.wait(topicFutures); - } + : "${Environment.cmsApi}${loadedMediaUrls.mediaUrls.first}"; } diff --git a/lib/pangea/course_plans/course_plan_room_extension.dart b/lib/pangea/course_plans/courses/course_plan_room_extension.dart similarity index 97% rename from lib/pangea/course_plans/course_plan_room_extension.dart rename to lib/pangea/course_plans/courses/course_plan_room_extension.dart index aedda789e..817499824 100644 --- a/lib/pangea/course_plans/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/courses/course_plan_room_extension.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_event.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_event.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; diff --git a/lib/pangea/course_plans/courses/course_plans_repo.dart b/lib/pangea/course_plans/courses/course_plans_repo.dart new file mode 100644 index 000000000..36011076a --- /dev/null +++ b/lib/pangea/course_plans/courses/course_plans_repo.dart @@ -0,0 +1,266 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_media_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_media/course_media_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_filter.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart'; +import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_response.dart'; +import 'package:fluffychat/pangea/payload_client/payload_client.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CoursePlansRepo { + static final Map> cache = {}; + static final GetStorage _courseStorage = GetStorage("course_storage"); + static const Duration cacheDuration = Duration(days: 1); + + static DateTime? get lastUpdated { + final entry = _courseStorage.read("last_updated"); + if (entry != null && entry is String) { + try { + return DateTime.parse(entry); + } catch (e) { + _courseStorage.remove("last_updated"); + } + } + return null; + } + + // TODO: Currently just getting one course plan at a time + // Should take advantage of batch fetching + static Future get( + GetLocalizedCoursesRequest request, + ) async { + if (request.coursePlanIds.length != 1) { + throw Exception("Get only supports single course plan ID"); + } + + await _courseStorage.initStorage; + final cached = _getCached(request); + if (cached != null) { + return cached; + } + + final uuid = request.coursePlanIds.first; + if (cache.containsKey(uuid)) { + return cache[uuid]!.future; + } + + final completer = Completer(); + cache[uuid] = completer; + + try { + final translation = await _fetch(request); + final coursePlan = translation.coursePlans[uuid]; + if (coursePlan == null) { + throw Exception("Course plan not found after translation"); + } + + await _setCached(coursePlan, uuid, request.l1); + completer.complete(coursePlan); + return coursePlan; + } catch (e) { + completer.completeError(e); + rethrow; + } finally { + cache.remove(uuid); + } + } + + static Future _fetch( + GetLocalizedCoursesRequest request, + ) async { + final Requests req = Requests( + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.getLocalizedCourse, + body: request.toJson(), + ); + + if (res.statusCode != 200) { + throw Exception( + "Failed to translate course plan: ${res.statusCode} ${res.body}", + ); + } + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + + final response = GetLocalizedCoursesResponse.fromJson(decodedBody); + + return response; + } + + static Future search( + GetLocalizedCoursesRequest request, + ) async { + await _courseStorage.initStorage; + + final missingIds = request.coursePlanIds + .where( + (id) => _courseStorage.read("${id}_${request.l1}") == null, + ) + .toList(); + + if (missingIds.isNotEmpty) { + final searchResult = await _fetch( + GetLocalizedCoursesRequest( + coursePlanIds: missingIds, + l1: request.l1, + ), + ); + + await _setCachedBatch( + searchResult, + request.l1, + ); + } + + return _getCachedBatch(request); + } + + static Future searchByFilter({ + required CourseFilter filter, + }) async { + final PayloadClient payload = PayloadClient( + baseUrl: Environment.cmsApi, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + // Run the search for the given filter, selecting only the course IDs + final result = await payload.find( + "course-plans", + (json) => json["id"] as String, + page: 1, + limit: 10, + where: filter.whereFilter, + select: {"id": true}, + ); + + return search( + GetLocalizedCoursesRequest( + coursePlanIds: result.docs, + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + ); + } + + static CoursePlanModel? _getCached( + GetLocalizedCoursesRequest request, + ) { + if (lastUpdated != null && + DateTime.now().difference(lastUpdated!) > cacheDuration) { + clearCache(); + return null; + } + + if (request.coursePlanIds.length != 1) { + throw Exception("Get cached only supports single course plan ID"); + } + + final uuid = request.coursePlanIds.first; + final cacheKey = "${uuid}_${request.l1}"; + final json = _courseStorage.read(cacheKey); + if (json != null) { + try { + return CoursePlanModel.fromJson(json); + } catch (e) { + _courseStorage.remove(cacheKey); + } + } + return null; + } + + static GetLocalizedCoursesResponse _getCachedBatch( + GetLocalizedCoursesRequest request, + ) { + if (lastUpdated != null && + DateTime.now().difference(lastUpdated!) > cacheDuration) { + clearCache(); + return GetLocalizedCoursesResponse(coursePlans: {}); + } + + final Map courses = {}; + for (final uuid in request.coursePlanIds) { + final cacheKey = "${uuid}_${request.l1}"; + final json = _courseStorage.read(cacheKey); + if (json != null) { + try { + final course = CoursePlanModel.fromJson( + Map.from(json), + ); + courses[uuid] = course; + } catch (e) { + _courseStorage.remove(cacheKey); + } + } + } + + return GetLocalizedCoursesResponse(coursePlans: courses); + } + + static Future _setCached( + CoursePlanModel course, + String courseId, + String l1, + ) async { + if (lastUpdated == null) { + await _courseStorage.write( + "last_updated", + DateTime.now().toIso8601String(), + ); + } + + final cacheKey = "${courseId}_$l1"; + await _courseStorage.write( + cacheKey, + course.toJson(), + ); + } + + static Future _setCachedBatch( + GetLocalizedCoursesResponse response, + String l1, + ) async { + if (lastUpdated == null) { + await _courseStorage.write( + "last_updated", + DateTime.now().toIso8601String(), + ); + } + + final List futures = response.coursePlans.entries.map((entry) { + final cacheKey = "${entry.key}_$l1"; + return _courseStorage.write( + cacheKey, + entry.value.toJson(), + ); + }).toList(); + + await Future.wait(futures); + } + + static Future clearCache() async { + final List futures = [ + CourseActivityRepo.clearCache(), + CourseLocationMediaRepo.clearCache(), + CourseLocationRepo.clearCache(), + CourseMediaRepo.clearCache(), + CourseTopicRepo.clearCache(), + _courseStorage.erase(), + ]; + + await Future.wait(futures); + } +} diff --git a/lib/pangea/course_plans/courses/get_localized_courses_request.dart b/lib/pangea/course_plans/courses/get_localized_courses_request.dart new file mode 100644 index 000000000..f29b1c848 --- /dev/null +++ b/lib/pangea/course_plans/courses/get_localized_courses_request.dart @@ -0,0 +1,23 @@ +class GetLocalizedCoursesRequest { + final List coursePlanIds; + final String l1; + + GetLocalizedCoursesRequest({ + required this.coursePlanIds, + required this.l1, + }); + + Map toJson() => { + "course_plan_ids": coursePlanIds, + "l1": l1, + }; + + factory GetLocalizedCoursesRequest.fromJson(Map json) { + return GetLocalizedCoursesRequest( + coursePlanIds: json['course_plan_ids'] != null + ? List.from(json['course_plan_ids']) + : [], + l1: json['l1'], + ); + } +} diff --git a/lib/pangea/course_plans/courses/get_localized_courses_response.dart b/lib/pangea/course_plans/courses/get_localized_courses_response.dart new file mode 100644 index 000000000..52bad4d0b --- /dev/null +++ b/lib/pangea/course_plans/courses/get_localized_courses_response.dart @@ -0,0 +1,28 @@ +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; + +class GetLocalizedCoursesResponse { + final Map coursePlans; + + GetLocalizedCoursesResponse({required this.coursePlans}); + + factory GetLocalizedCoursesResponse.fromJson(Map json) { + final plansEntry = json['course_plans'] as Map; + return GetLocalizedCoursesResponse( + coursePlans: plansEntry.map( + (key, value) => MapEntry( + key, + CoursePlanModel.fromJson(value), + ), + ), + ); + } + + Map toJson() => { + "course_plans": coursePlans.map( + (key, value) => MapEntry( + key, + value.toJson(), + ), + ), + }; +} diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index 4790bf5e7..aad07bfea 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -12,20 +12,19 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/course_plans/activity_summaries_provider.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; class CourseSettings extends StatefulWidget { final Room room; - final CoursePlanController controller; - const CourseSettings( - this.controller, { + const CourseSettings({ super.key, required this.room, }); @@ -35,14 +34,43 @@ class CourseSettings extends StatefulWidget { } class CourseSettingsState extends State - with ActivitySummariesProvider { - CoursePlanController get controller => widget.controller; + with ActivitySummariesProvider, CoursePlanProvider { Room get room => widget.room; + bool _loadingActivities = true; @override void initState() { super.initState(); _loadSummaries(); + if (widget.room.coursePlan != null) { + _loadCourseInfo(); + } + } + + @override + void didUpdateWidget(covariant CourseSettings oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.room.id != widget.room.id) { + if (widget.room.coursePlan != null) { + _loadCourseInfo(); + } + } + } + + Future _loadCourseInfo() async { + setState(() => _loadingActivities = true); + await loadCourse(widget.room.coursePlan!.uuid); + if (course != null) { + await loadTopics(); + + final futures = []; + for (final topicId in course!.topicIds) { + futures.add(loadActivity(topicId)); + } + await Future.wait(futures); + } + + if (mounted) setState(() => _loadingActivities = false); } Future _loadSummaries() async { @@ -64,11 +92,11 @@ class CourseSettingsState extends State @override Widget build(BuildContext context) { - if (controller.loading) { + if (loadingCourse) { return const Center(child: CircularProgressIndicator.adaptive()); } - if (controller.course == null || controller.error != null) { + if (course == null || courseError != null) { return room.canChangeStateEvent(PangeaEventTypes.coursePlan) ? Column( spacing: 50.0, @@ -114,149 +142,172 @@ class CourseSettingsState extends State final double descFontSize = isColumnMode ? 12.0 : 8.0; final double iconSize = isColumnMode ? 16.0 : 12.0; - final course = controller.course!; - final topicIndex = currentTopicIndex( + if (loadingTopics) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + final activeTopicId = currentTopicId( room.client.userID!, - course, + course!, ); - return FutureBuilder( - future: topicsToUsers(room, course), - builder: (context, snapshot) { - final topicsToUsers = snapshot.data ?? {}; - return Column( - spacing: isColumnMode ? 40.0 : 36.0, - mainAxisSize: MainAxisSize.min, - children: course.loadedTopics.mapIndexed((index, topic) { - final unlocked = index <= topicIndex; - final usersInTopic = topicsToUsers[topic.uuid] ?? []; - final activities = topic.loadedActivities; - activities.sort( - (a, b) => a.req.numberOfParticipants.compareTo( - b.req.numberOfParticipants, - ), - ); - return AbsorbPointer( - absorbing: !unlocked, - child: Opacity( - opacity: unlocked ? 1.0 : 0.5, - child: Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - LayoutBuilder( - builder: (context, constraints) { - return Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Row( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, + final int? topicIndex = + activeTopicId == null ? null : course!.topicIds.indexOf(activeTopicId); + + final Map> userTopics = _loadingActivities + ? {} + : topicsToUsers( + room, + course!, + ); + + return Column( + spacing: isColumnMode ? 40.0 : 36.0, + mainAxisSize: MainAxisSize.min, + children: course!.topicIds.mapIndexed((index, topicId) { + final topic = course!.loadedTopics[topicId]; + if (topic == null) { + return const SizedBox(); + } + + final usersInTopic = userTopics[topicId] ?? []; + final activityError = activityErrors[topicId]; + + final bool locked = topicIndex == null ? false : index > topicIndex; + final disabled = locked || _loadingActivities || activityError != null; + return AbsorbPointer( + absorbing: disabled, + child: Opacity( + opacity: disabled ? 0.5 : 1.0, + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + LayoutBuilder( + builder: (context, constraints) { + return Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( children: [ - Stack( - children: [ - ClipPath( - clipper: PinClipper(), - child: ImageByUrl( - imageUrl: topic.imageUrl, - width: 54.0, - replacement: Container( - width: 54.0, - height: 54.0, - decoration: BoxDecoration( - color: - theme.colorScheme.secondary, - ), - ), + ClipPath( + clipper: PinClipper(), + child: ImageByUrl( + imageUrl: topic.imageUrl, + width: 54.0, + replacement: Container( + width: 54.0, + height: 54.0, + decoration: BoxDecoration( + color: theme.colorScheme.secondary, ), ), - if (!unlocked) - const Positioned( - bottom: 0, - right: 0, - child: Icon(Icons.lock, size: 24.0), - ), - ], - ), - Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - topic.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: titleFontSize, - ), - ), - if (topic.location != null) - CourseInfoChip( - icon: Icons.location_on, - text: topic.location!, - fontSize: descFontSize, - iconSize: iconSize, - ), - if (constraints.maxWidth < 700.0) - Padding( - padding: const EdgeInsetsGeometry - .symmetric( - vertical: 4.0, - ), - child: TopicParticipantList( - room: room, - users: usersInTopic, - avatarSize: - isColumnMode ? 50.0 : 25.0, - overlap: - isColumnMode ? 20.0 : 8.0, - ), - ), - ], ), ), + if (locked) + const Positioned( + bottom: 0, + right: 0, + child: Icon(Icons.lock, size: 24.0), + ) + else if (_loadingActivities) + const SizedBox( + width: 54.0, + height: 54.0, + child: + CircularProgressIndicator.adaptive(), + ), ], ), - ), - if (constraints.maxWidth >= 700.0) - TopicParticipantList( - room: room, - users: usersInTopic, - avatarSize: isColumnMode ? 50.0 : 25.0, - overlap: isColumnMode ? 20.0 : 8.0, + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + topic.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: titleFontSize, + ), + ), + if (topic.location != null) + CourseInfoChip( + icon: Icons.location_on, + text: topic.location!, + fontSize: descFontSize, + iconSize: iconSize, + ), + if (constraints.maxWidth < 700.0) + Padding( + padding: + const EdgeInsetsGeometry.symmetric( + vertical: 4.0, + ), + child: TopicParticipantList( + room: room, + users: usersInTopic, + avatarSize: + isColumnMode ? 50.0 : 25.0, + overlap: isColumnMode ? 20.0 : 8.0, + ), + ), + ], + ), ), - ], - ); - }, - ), - if (unlocked) - SizedBox( - height: isColumnMode ? 290.0 : 210.0, - child: TopicActivitiesList( - room: room, - activities: activities, - controller: this, + ], + ), ), - ), - ], + if (constraints.maxWidth >= 700.0) + TopicParticipantList( + room: room, + users: usersInTopic, + avatarSize: isColumnMode ? 50.0 : 25.0, + overlap: isColumnMode ? 20.0 : 8.0, + ), + ], + ); + }, ), - ), - ); - }).toList(), + if (!locked) + _loadingActivities + ? const Center( + child: CircularProgressIndicator.adaptive(), + ) + : activityError != null + ? ErrorIndicator( + message: L10n.of(context).oopsSomethingWentWrong, + ) + : topic.loadedActivities.isNotEmpty + ? SizedBox( + height: isColumnMode ? 290.0 : 210.0, + child: TopicActivitiesList( + room: room, + activities: topic.loadedActivities, + controller: this, + ), + ) + : const SizedBox(), + ], + ), + ), ); - }, + }).toList(), ); } } class TopicActivitiesList extends StatefulWidget { final Room room; - final List activities; + final Map activities; final CourseSettingsState controller; const TopicActivitiesList({ @@ -282,26 +333,33 @@ class TopicActivitiesListState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final isColumnMode = FluffyThemes.isColumnMode(context); + + final activityEntries = widget.activities.entries.toList(); + activityEntries.sort( + (a, b) => a.value.req.numberOfParticipants.compareTo( + b.value.req.numberOfParticipants, + ), + ); + return Scrollbar( thumbVisibility: true, controller: _scrollController, child: ListView.builder( controller: _scrollController, scrollDirection: Axis.horizontal, - itemCount: widget.activities.length, + itemCount: activityEntries.length, itemBuilder: (context, index) { - final activityId = widget.activities[index].activityId; - + final activityEntry = activityEntries[index]; final complete = widget.controller.hasCompletedActivity( widget.room.client.userID!, - activityId, + activityEntry.key, ); final activityRoomId = widget.room.activeActivityRoomId( - activityId, + activityEntry.key, ); - final activity = widget.activities[index]; + final activity = activityEntry.value; return Padding( padding: const EdgeInsets.only(right: 24.0), child: MouseRegion( @@ -310,7 +368,7 @@ class TopicActivitiesListState extends State { onTap: () => context.go( activityRoomId != null ? "/rooms/spaces/${widget.room.id}/$activityRoomId" - : "/rooms/spaces/${widget.room.id}/activity/$activityId", + : "/rooms/spaces/${widget.room.id}/activity/${activityEntry.key}", ), child: Stack( children: [ diff --git a/lib/pangea/login/pages/create_pangea_account_page.dart b/lib/pangea/login/pages/create_pangea_account_page.dart index 7829c5990..43e55e4bc 100644 --- a/lib/pangea/login/pages/create_pangea_account_page.dart +++ b/lib/pangea/login/pages/create_pangea_account_page.dart @@ -10,8 +10,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; import 'package:fluffychat/pangea/login/utils/lang_code_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -99,7 +100,12 @@ class CreatePangeaAccountPageState extends State { throw Exception('No course plan associated with space $spaceId'); } - final course = await CoursePlansRepo.get(courseId); + final course = await CoursePlansRepo.get( + GetLocalizedCoursesRequest( + coursePlanIds: [courseId], + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), + ); _spaceId = spaceId; _courseLangCode = course.targetLanguage; diff --git a/lib/pangea/login/pages/new_trip_page.dart b/lib/pangea/login/pages/new_trip_page.dart index b00b407cc..98dee3593 100644 --- a/lib/pangea/login/pages/new_trip_page.dart +++ b/lib/pangea/login/pages/new_trip_page.dart @@ -43,6 +43,7 @@ class NewTripPageState extends State with CourseSearchProvider { Widget build(BuildContext context) { final theme = Theme.of(context); final spaceId = widget.spaceId; + final courseEntries = courses.entries.toList(); return Scaffold( appBar: AppBar( title: Row( @@ -142,14 +143,15 @@ class NewTripPageState extends State with CourseSearchProvider { child: ListView.separated( separatorBuilder: (context, index) => const SizedBox(height: 10.0), - itemCount: courses.length, + itemCount: courseEntries.length, itemBuilder: (context, index) { - final course = courses[index]; + final course = courseEntries[index].value; + final courseId = courseEntries[index].key; return InkWell( onTap: () => context.go( spaceId != null - ? '/rooms/spaces/$spaceId/addcourse/${courses[index].uuid}' - : '/${widget.route}/course/own/${course.uuid}', + ? '/rooms/spaces/$spaceId/addcourse/$courseId' + : '/${widget.route}/course/own/$courseId', ), borderRadius: BorderRadius.circular(12.0), child: Container( @@ -195,7 +197,7 @@ class NewTripPageState extends State with CourseSearchProvider { ], ), CourseInfoChips( - course, + courseId, iconSize: 12.0, fontSize: 12.0, ), diff --git a/lib/pangea/login/pages/public_trip_page.dart b/lib/pangea/login/pages/public_trip_page.dart index 30efa16ae..9d54fe19a 100644 --- a/lib/pangea/login/pages/public_trip_page.dart +++ b/lib/pangea/login/pages/public_trip_page.dart @@ -9,8 +9,9 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/courses/course_plans_repo.dart'; +import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/spaces/utils/public_course_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -107,13 +108,17 @@ class PublicTripPageState extends State { } try { - final searchResult = await CoursePlansRepo.search( - discoveredCourses.map((c) => c.courseId).toList(), + final resp = await CoursePlansRepo.search( + GetLocalizedCoursesRequest( + coursePlanIds: discoveredCourses.map((c) => c.courseId).toList(), + l1: MatrixState.pangeaController.languageController.activeL1Code()!, + ), ); + final searchResult = resp.coursePlans; coursePlans.clear(); - for (final course in searchResult) { - coursePlans[course.uuid] = course; + for (final entry in searchResult.entries) { + coursePlans[entry.key] = entry.value; } } catch (e, s) { ErrorHandler.logError( @@ -240,8 +245,8 @@ class PublicTripPageState extends State { } final roomChunk = filteredCourses[index].room; - final course = - coursePlans[filteredCourses[index].courseId]; + final courseId = filteredCourses[index].courseId; + final course = coursePlans[courseId]; final displayname = roomChunk.name ?? roomChunk.canonicalAlias ?? @@ -249,7 +254,7 @@ class PublicTripPageState extends State { return InkWell( onTap: () => context.go( - '/${widget.route}/course/public/${filteredCourses[index].courseId}', + '/${widget.route}/course/public/$courseId', extra: roomChunk, ), borderRadius: BorderRadius.circular(12.0), @@ -296,7 +301,7 @@ class PublicTripPageState extends State { ), if (course != null) ...[ CourseInfoChips( - course, + courseId, iconSize: 12.0, fontSize: 12.0, ), diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart deleted file mode 100644 index 03a267214..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/payload_client/join_field.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan from the CMS API -class CmsCoursePlan { - static const String slug = "course-plans"; - final String id; - final String title; - final String description; - final String cefrLevel; - final String l1; // Language of instruction - final String l2; // Target language - final JoinField? coursePlanMedia; - final JoinField? coursePlanTopics; - final String? coursePlanTranslationGroup; - final String? originalCoursePlan; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String updatedAt; - final String createdAt; - - CmsCoursePlan({ - required this.id, - required this.title, - required this.description, - required this.cefrLevel, - required this.l1, - required this.l2, - this.coursePlanMedia, - this.coursePlanTopics, - this.coursePlanTranslationGroup, - this.originalCoursePlan, - this.createdBy, - this.updatedBy, - required this.updatedAt, - required this.createdAt, - }); - - factory CmsCoursePlan.fromJson(Map json) { - return CmsCoursePlan( - id: json['id'], - title: json['title'], - description: json['description'], - cefrLevel: json['cefrLevel'], - l1: json['l1'], - l2: json['l2'], - coursePlanMedia: JoinField.fromJson(json['coursePlanMedia']), - coursePlanTopics: JoinField.fromJson(json['coursePlanTopics']), - coursePlanTranslationGroup: json['coursePlanTranslationGroup'], - originalCoursePlan: json['originalCoursePlan'], - createdBy: PolymorphicRelationship.fromJson(json['createdBy']), - updatedBy: PolymorphicRelationship.fromJson(json['updatedBy']), - updatedAt: json['updatedAt'], - createdAt: json['createdAt'], - ); - } - - Map toJson() { - return { - 'id': id, - 'title': title, - 'description': description, - 'cefrLevel': cefrLevel, - 'l1': l1, - 'l2': l2, - 'coursePlanMedia': coursePlanMedia?.toJson(), - 'coursePlanTopics': coursePlanTopics?.toJson(), - 'coursePlanTranslationGroup': coursePlanTranslationGroup, - 'originalCoursePlan': originalCoursePlan, - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'updatedAt': updatedAt, - 'createdAt': createdAt, - }; - } - - CoursePlanModel toCoursePlanModel() { - return CoursePlanModel( - uuid: id, - targetLanguage: l2, - languageOfInstructions: l1, - cefrLevel: LanguageLevelTypeEnumExtension.fromString(cefrLevel), - title: title, - description: description, - mediaIds: coursePlanMedia?.docs ?? [], - topicIds: coursePlanTopics?.docs ?? [], - updatedAt: DateTime.parse(updatedAt), - createdAt: DateTime.parse(createdAt), - ); - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart deleted file mode 100644 index c688846fe..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:fluffychat/pangea/activity_generator/media_enum.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/payload_client/join_field.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan activity role -class CmsCoursePlanActivityRole { - final String id; - final String name; - final String goal; - final String? avatarUrl; - - CmsCoursePlanActivityRole({ - required this.id, - required this.name, - required this.goal, - this.avatarUrl, - }); - - factory CmsCoursePlanActivityRole.fromJson(Map json) { - return CmsCoursePlanActivityRole( - id: json['id'] as String, - name: json['name'] as String, - goal: json['goal'] as String, - avatarUrl: json['avatarUrl'] as String?, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'goal': goal, - 'avatarUrl': avatarUrl, - }; - } - - ActivityRole toActivityRole() { - return ActivityRole( - id: id, - name: name, - goal: goal, - avatarUrl: avatarUrl, - ); - } -} - -/// Represents vocabulary in a course plan activity -class CmsCoursePlanVocab { - final String lemma; - final String pos; - final String? id; - - CmsCoursePlanVocab({ - required this.lemma, - required this.pos, - this.id, - }); - - factory CmsCoursePlanVocab.fromJson(Map json) { - return CmsCoursePlanVocab( - lemma: json['lemma'] as String, - pos: json['pos'] as String, - id: json['id'] as String?, - ); - } - - Map toJson() { - return { - 'lemma': lemma, - 'pos': pos, - 'id': id, - }; - } -} - -/// Represents a course plan activity from the CMS API -class CmsCoursePlanActivity { - static const String slug = "course-plan-activities"; - - final String id; - final String title; - final String description; - final String learningObjective; - final String instructions; - final String l1; // Language of instruction - final String l2; // Target language - final String mode; - final LanguageLevelTypeEnum cefrLevel; - final List roles; - final List vocabs; - final JoinField? coursePlanActivityMedia; - final List coursePlanTopics; - final String? coursePlanActivityTranslationGroup; - final String? originalCoursePlanActivity; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String updatedAt; - final String createdAt; - - CmsCoursePlanActivity({ - required this.id, - required this.title, - required this.description, - required this.learningObjective, - required this.instructions, - required this.l1, - required this.l2, - required this.mode, - required this.cefrLevel, - required this.roles, - required this.vocabs, - required this.coursePlanActivityMedia, - required this.coursePlanTopics, - this.coursePlanActivityTranslationGroup, - this.originalCoursePlanActivity, - this.createdBy, - this.updatedBy, - required this.updatedAt, - required this.createdAt, - }); - - factory CmsCoursePlanActivity.fromJson(Map json) { - return CmsCoursePlanActivity( - id: json['id'] as String, - title: json['title'] as String, - description: json['description'] as String, - learningObjective: json['learningObjective'] as String, - instructions: json['instructions'] as String, - l1: json['l1'] as String, - l2: json['l2'] as String, - mode: json['mode'] as String, - cefrLevel: LanguageLevelTypeEnumExtension.fromString( - json['cefrLevel'] as String, - ), - roles: (json['roles'] as List) - .map( - (role) => CmsCoursePlanActivityRole.fromJson( - role as Map, - ), - ) - .toList(), - vocabs: (json['vocabs'] as List) - .map( - (vocab) => - CmsCoursePlanVocab.fromJson(vocab as Map), - ) - .toList(), - coursePlanActivityMedia: - JoinField.fromJson(json['coursePlanActivityMedia']), - coursePlanTopics: List.from(json['coursePlanTopics']), - coursePlanActivityTranslationGroup: - json['coursePlanActivityTranslationGroup'] as String?, - originalCoursePlanActivity: json['originalCoursePlanActivity'] as String?, - createdBy: json['createdBy'] != null - ? PolymorphicRelationship.fromJson(json['createdBy']) - : null, - updatedBy: json['updatedBy'] != null - ? PolymorphicRelationship.fromJson(json['updatedBy']) - : null, - updatedAt: json['updatedAt'] as String, - createdAt: json['createdAt'] as String, - ); - } - - Map toJson() { - return { - 'id': id, - 'title': title, - 'description': description, - 'learningObjective': learningObjective, - 'instructions': instructions, - 'l1': l1, - 'l2': l2, - 'mode': mode, - 'cefrLevel': cefrLevel.string, - 'roles': roles.map((role) => role.toJson()).toList(), - 'vocabs': vocabs.map((vocab) => vocab.toJson()).toList(), - 'coursePlanActivityMedia': coursePlanActivityMedia?.toJson(), - 'coursePlanTopics': coursePlanTopics, - 'coursePlanActivityTranslationGroup': coursePlanActivityTranslationGroup, - 'originalCoursePlanActivity': originalCoursePlanActivity, - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'updatedAt': updatedAt, - 'createdAt': createdAt, - }; - } - - ActivityPlanModel toActivityPlanModel(String? imageUrl) { - return ActivityPlanModel( - req: ActivityPlanRequest( - topic: "", - mode: mode, - objective: "", - media: MediaEnum.nan, - cefrLevel: cefrLevel, - languageOfInstructions: l1, - targetLanguage: l2, - numberOfParticipants: roles.length, - ), - activityId: id, - title: title, - description: description, - learningObjective: learningObjective, - instructions: instructions, - vocab: vocabs.map((v) => Vocab(lemma: v.lemma, pos: v.pos)).toList(), - roles: Map.fromEntries( - roles.map((role) => MapEntry(role.id, role.toActivityRole())), - ), - imageURL: imageUrl, - ); - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location.dart deleted file mode 100644 index 37b8eedf5..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:fluffychat/pangea/payload_client/join_field.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan activity location from the CMS API -class CmsCoursePlanActivityLocation { - static const String slug = "course-plan-activity-locations"; - final String id; - final String name; - // [longitude, latitude] - minItems: 2, maxItems: 2 - final List? coordinates; - final JoinField? coursePlanActivityLocationMedia; - final List coursePlanActivities; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String updatedAt; - final String createdAt; - - CmsCoursePlanActivityLocation({ - required this.id, - required this.name, - this.coordinates, - this.coursePlanActivityLocationMedia, - required this.coursePlanActivities, - this.createdBy, - this.updatedBy, - required this.updatedAt, - required this.createdAt, - }); - - factory CmsCoursePlanActivityLocation.fromJson(Map json) { - return CmsCoursePlanActivityLocation( - id: json['id'] as String, - name: json['name'] as String, - coordinates: (json['coordinates'] as List?) - ?.map((coord) => (coord as num).toDouble()) - .toList(), - coursePlanActivityLocationMedia: - json['coursePlanActivityLocationMedia'] != null - ? JoinField.fromJson(json['coursePlanActivityLocationMedia']) - : null, - coursePlanActivities: List.from(json['coursePlanActivities']), - createdBy: json['createdBy'] != null - ? PolymorphicRelationship.fromJson(json['createdBy']) - : null, - updatedBy: json['updatedBy'] != null - ? PolymorphicRelationship.fromJson(json['updatedBy']) - : null, - updatedAt: json['updatedAt'] as String, - createdAt: json['createdAt'] as String, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'coordinates': coordinates, - 'coursePlanActivities': coursePlanActivities, - 'coursePlanActivityLocationMedia': - coursePlanActivityLocationMedia?.toJson(), - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'updatedAt': updatedAt, - 'createdAt': createdAt, - }; - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location_media.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location_media.dart deleted file mode 100644 index 819869279..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_location_media.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:fluffychat/pangea/payload_client/image_sizes.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan activity location media from the CMS API -class CmsCoursePlanActivityLocationMedia { - static const String slug = "course-plan-activity-location-media"; - final String id; - final String? alt; - final List coursePlanActivityLocations; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String? prefix; - final String updatedAt; - final String createdAt; - final String? url; - final String? thumbnailURL; - final String? filename; - final String? mimeType; - final int? filesize; - final int? width; - final int? height; - final double? focalX; - final double? focalY; - final ImageSizes? sizes; - - CmsCoursePlanActivityLocationMedia({ - required this.id, - this.alt, - required this.coursePlanActivityLocations, - this.createdBy, - this.updatedBy, - this.prefix, - required this.updatedAt, - required this.createdAt, - this.url, - this.thumbnailURL, - this.filename, - this.mimeType, - this.filesize, - this.width, - this.height, - this.focalX, - this.focalY, - this.sizes, - }); - - factory CmsCoursePlanActivityLocationMedia.fromJson( - Map json, - ) { - return CmsCoursePlanActivityLocationMedia( - id: json['id'], - alt: json['alt'], - coursePlanActivityLocations: - List.from(json['coursePlanActivityLocations'] as List), - createdBy: json['createdBy'] != null - ? PolymorphicRelationship.fromJson(json['createdBy']) - : null, - updatedBy: json['updatedBy'] != null - ? PolymorphicRelationship.fromJson(json['updatedBy']) - : null, - prefix: json['prefix'], - updatedAt: json['updatedAt'], - createdAt: json['createdAt'], - url: json['url'], - thumbnailURL: json['thumbnailURL'], - filename: json['filename'], - mimeType: json['mimeType'], - filesize: json['filesize'], - width: json['width'], - height: json['height'], - focalX: json['focalX']?.toDouble(), - focalY: json['focalY']?.toDouble(), - ); - } - - Map toJson() { - return { - 'id': id, - 'alt': alt, - 'coursePlanActivityLocations': coursePlanActivityLocations, - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'prefix': prefix, - 'updatedAt': updatedAt, - 'createdAt': createdAt, - 'url': url, - 'thumbnailURL': thumbnailURL, - 'filename': filename, - 'mimeType': mimeType, - 'filesize': filesize, - 'width': width, - 'height': height, - 'focalX': focalX, - 'focalY': focalY, - 'sizes': sizes?.toJson(), - }; - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart deleted file mode 100644 index b3b272bb2..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:fluffychat/pangea/payload_client/image_sizes.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents course plan activity media from the CMS API -class CmsCoursePlanActivityMedia { - static const String slug = "course-plan-activity-media"; - - final String id; - final String? alt; - final List coursePlanActivities; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String? prefix; - final String updatedAt; - final String createdAt; - final String? url; - final String? thumbnailURL; - final String? filename; - final String? mimeType; - final int? filesize; - final int? width; - final int? height; - final double? focalX; - final double? focalY; - final ImageSizes? sizes; - - CmsCoursePlanActivityMedia({ - required this.id, - this.alt, - required this.coursePlanActivities, - this.createdBy, - this.updatedBy, - this.prefix, - required this.updatedAt, - required this.createdAt, - this.url, - this.thumbnailURL, - this.filename, - this.mimeType, - this.filesize, - this.width, - this.height, - this.focalX, - this.focalY, - this.sizes, - }); - - factory CmsCoursePlanActivityMedia.fromJson(Map json) { - return CmsCoursePlanActivityMedia( - id: json['id'] as String, - alt: json['alt'] as String?, - coursePlanActivities: List.from(json['coursePlanActivities']), - createdBy: json['createdBy'] != null - ? PolymorphicRelationship.fromJson(json['createdBy']) - : null, - updatedBy: json['updatedBy'] != null - ? PolymorphicRelationship.fromJson(json['updatedBy']) - : null, - prefix: json['prefix'] as String?, - updatedAt: json['updatedAt'] as String, - createdAt: json['createdAt'] as String, - url: json['url'] as String?, - thumbnailURL: json['thumbnailURL'] as String?, - filename: json['filename'] as String?, - mimeType: json['mimeType'] as String?, - filesize: json['filesize'] as int?, - width: json['width'] as int?, - height: json['height'] as int?, - focalX: (json['focalX'] as num?)?.toDouble(), - focalY: (json['focalY'] as num?)?.toDouble(), - sizes: json['sizes'] != null ? ImageSizes.fromJson(json['sizes']) : null, - ); - } - - Map toJson() { - return { - 'id': id, - 'alt': alt, - 'coursePlanActivities': coursePlanActivities, - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'prefix': prefix, - 'updatedAt': updatedAt, - 'createdAt': createdAt, - 'url': url, - 'thumbnailURL': thumbnailURL, - 'filename': filename, - 'mimeType': mimeType, - 'filesize': filesize, - 'width': width, - 'height': height, - 'focalX': focalX, - 'focalY': focalY, - 'sizes': sizes?.toJson(), - }; - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic.dart deleted file mode 100644 index feabb3128..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:fluffychat/pangea/course_plans/course_topic_model.dart'; -import 'package:fluffychat/pangea/payload_client/join_field.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan topic from the CMS API -class CmsCoursePlanTopic { - static const String slug = "course-plan-topics"; - final String id; - final String title; - final String description; - final JoinField? coursePlanActivities; - final JoinField? coursePlanTopicLocations; - final List coursePlans; - final String? coursePlanTopicTranslationGroup; - final String? originalCoursePlanTopic; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String updatedAt; - final String createdAt; - - CmsCoursePlanTopic({ - required this.id, - required this.title, - required this.description, - required this.coursePlanActivities, - required this.coursePlanTopicLocations, - required this.coursePlans, - this.coursePlanTopicTranslationGroup, - this.originalCoursePlanTopic, - this.createdBy, - this.updatedBy, - required this.updatedAt, - required this.createdAt, - }); - - factory CmsCoursePlanTopic.fromJson(Map json) { - return CmsCoursePlanTopic( - id: json['id'] as String, - title: json['title'] as String, - description: json['description'] as String, - coursePlanActivities: JoinField.fromJson( - json['coursePlanActivities'], - ), - coursePlanTopicLocations: JoinField.fromJson( - json['coursePlanTopicLocations'], - ), - coursePlans: List.from(json['coursePlans']), - coursePlanTopicTranslationGroup: - json['coursePlanTopicTranslationGroup'] as String?, - originalCoursePlanTopic: json['originalCoursePlanTopic'] as String?, - createdBy: json['createdBy'] != null - ? PolymorphicRelationship.fromJson(json['createdBy']) - : null, - updatedBy: json['updatedBy'] != null - ? PolymorphicRelationship.fromJson(json['updatedBy']) - : null, - updatedAt: json['updatedAt'] as String, - createdAt: json['createdAt'] as String, - ); - } - - Map toJson() { - return { - 'id': id, - 'title': title, - 'description': description, - 'coursePlanActivities': coursePlanActivities?.toJson(), - 'coursePlanTopicLocations': coursePlanTopicLocations?.toJson(), - 'coursePlans': coursePlans, - 'coursePlanTopicTranslationGroup': coursePlanTopicTranslationGroup, - 'originalCoursePlanTopic': originalCoursePlanTopic, - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'updatedAt': updatedAt, - 'createdAt': createdAt, - }; - } - - CourseTopicModel toCourseTopicModel() { - return CourseTopicModel( - title: title, - description: description, - uuid: id, - activityIds: coursePlanActivities?.docs ?? [], - locationIds: coursePlanTopicLocations?.docs ?? [], - ); - } -} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart index 343d36523..6cc3c5365 100644 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_topic_location.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pangea/course_plans/course_location_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_locations/course_location_model.dart'; import 'package:fluffychat/pangea/payload_client/join_field.dart'; import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_translation_group.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_translation_group.dart deleted file mode 100644 index 2b4f3a51a..000000000 --- a/lib/pangea/payload_client/models/course_plan/cms_course_plan_translation_group.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:fluffychat/pangea/payload_client/join_field.dart'; -import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; - -/// Represents a course plan translation group from the CMS API -class CmsCoursePlanTranslationGroup { - static const String slug = "course-plans"; - final String id; - final JoinField? coursePlans; - final PolymorphicRelationship? createdBy; - final PolymorphicRelationship? updatedBy; - final String updatedAt; - final String createdAt; - - CmsCoursePlanTranslationGroup({ - required this.id, - this.coursePlans, - this.createdBy, - this.updatedBy, - required this.updatedAt, - required this.createdAt, - }); - - factory CmsCoursePlanTranslationGroup.fromJson(Map json) { - return CmsCoursePlanTranslationGroup( - id: json['id'], - coursePlans: JoinField.fromJson(json['coursePlans']), - createdBy: PolymorphicRelationship.fromJson(json['createdBy']), - updatedBy: PolymorphicRelationship.fromJson(json['updatedBy']), - updatedAt: json['updatedAt'], - createdAt: json['createdAt'], - ); - } - - Map toJson() { - return { - 'id': id, - 'coursePlans': coursePlans?.toJson(), - 'createdBy': createdBy?.toJson(), - 'updatedBy': updatedBy?.toJson(), - 'updatedAt': updatedAt, - 'createdAt': createdAt, - }; - } -}