Organize course repos (#4262)
* integrate CoursePlansRepo.translateActivity, translateTopic, translateCoursePlan * move translation functions to requisite files * integrate translation endpoint * refactor: reorganize course-related repos, add request and response model classes * remove l2s from translation requests * update translation request and response models, use translation endpoint to get course info, cache courses with L1s in cache key * update topics repo to use translation endpoint * use activity translation endpoint * refactor: incremental loading of individual course info, account for discrepancy between translated IDs and original IDs * incremental loading of course batches * Update lib/pangea/course_plans/courses/course_plan_room_extension.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * cleanup * cleanup * fix: some name changes * formatting --------- Co-authored-by: WilsonLe <leanhminh2907@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com>
This commit is contained in:
parent
181c4a369b
commit
261e28abb7
60 changed files with 2412 additions and 2666 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<ActivitySessionStartPage>
|
||||
with ActivitySummariesProvider {
|
||||
ActivityPlanModel? activity;
|
||||
CoursePlanModel? course;
|
||||
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
|
@ -264,14 +262,14 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
|
|||
}
|
||||
|
||||
Future<void> _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");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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<CourseChats>
|
|||
bool noMoreRooms = false;
|
||||
bool isLoading = false;
|
||||
|
||||
CoursePlanModel? course;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
loadHierarchy(reload: true);
|
||||
|
|
@ -93,12 +90,6 @@ class CourseChatsController extends State<CourseChats>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void setCourse(CoursePlanModel? course) {
|
||||
setState(() {
|
||||
this.course = course;
|
||||
});
|
||||
}
|
||||
|
||||
Set<String> get childrenIds =>
|
||||
room?.spaceChildren.map((c) => c.roomId).whereType<String>().toSet() ??
|
||||
{};
|
||||
|
|
@ -148,7 +139,6 @@ class CourseChatsController extends State<CourseChats>
|
|||
sessionsMap[activity]!.add(
|
||||
ExtendedSpaceRoomsChunk(
|
||||
chunk: chunk,
|
||||
activityId: activity.activityId,
|
||||
userIds: users,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
class ExtendedSpaceRoomsChunk {
|
||||
final SpaceRoomsChunk chunk;
|
||||
final String activityId;
|
||||
final List<String> userIds;
|
||||
|
||||
ExtendedSpaceRoomsChunk({
|
||||
required this.chunk,
|
||||
required this.activityId,
|
||||
required this.userIds,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CourseInfoChips> createState() => CourseInfoChipsState();
|
||||
}
|
||||
|
||||
class CourseInfoChipsState extends State<CourseInfoChips>
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<CourseInvitePage> {
|
||||
class CourseInvitePageController extends State<CourseInvitePage>
|
||||
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<String> getSpaceId() async {
|
||||
if (widget.courseCreationCompleter == null) {
|
||||
throw Exception("No course creation completer provided");
|
||||
|
|
@ -44,184 +59,176 @@ class CourseInvitePageController extends State<CourseInvitePage> {
|
|||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<T extends StatefulWidget> on State<T> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
List<CoursePlanModel> courses = [];
|
||||
Map<String, CoursePlanModel> courses = {};
|
||||
LanguageModel? targetLanguageFilter;
|
||||
|
||||
@override
|
||||
|
|
@ -36,7 +37,8 @@ mixin CourseSearchProvider<T extends StatefulWidget> on State<T> {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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<SelectedCourse> {
|
||||
class SelectedCourseController extends State<SelectedCourse>
|
||||
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<SelectedCourse> {
|
|||
Future<void> 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<void> launchCourse(CoursePlanModel course) async {
|
||||
Future<void> launchCourse(
|
||||
String courseId,
|
||||
CoursePlanModel course,
|
||||
) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final Completer<String> completer = Completer<String>();
|
||||
client
|
||||
|
|
@ -88,7 +107,7 @@ class SelectedCourseController extends State<SelectedCourse> {
|
|||
sdk.StateEvent(
|
||||
type: PangeaEventTypes.coursePlan,
|
||||
content: {
|
||||
"uuid": course.uuid,
|
||||
"uuid": courseId,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -129,7 +148,7 @@ class SelectedCourseController extends State<SelectedCourse> {
|
|||
context.go("/rooms/spaces/${space.id}/details");
|
||||
}
|
||||
|
||||
Future<void> joinCourse(CoursePlanModel course) async {
|
||||
Future<void> joinCourse() async {
|
||||
if (widget.roomChunk == null) {
|
||||
throw Exception("Room chunk is null");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<T extends StatefulWidget> on State<T> {
|
||||
|
|
@ -56,6 +56,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
),
|
||||
)
|
||||
.map((e) => e.activityPlan.activityId)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
}
|
||||
|
||||
|
|
@ -70,20 +71,11 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
|
|||
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<T extends StatefulWidget> on State<T> {
|
|||
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<Map<String, List<User>>> topicsToUsers(
|
||||
Map<String, List<User>> topicsToUsers(
|
||||
Room room,
|
||||
CoursePlanModel course,
|
||||
) async {
|
||||
) {
|
||||
final Map<String, List<User>> 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;
|
||||
|
|
@ -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<String, Completer<TranslateActivityResponse>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_activity_storage');
|
||||
|
||||
static Future<TranslateActivityResponse> 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<TranslateActivityResponse> 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<TranslateActivityResponse> _fetch(
|
||||
TranslateActivityRequest request,
|
||||
String batchId,
|
||||
) async {
|
||||
if (_cache.containsKey(batchId)) {
|
||||
return _cache[batchId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<TranslateActivityResponse>();
|
||||
_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<String, ActivityPlanModel> 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<Map<String, dynamic>>(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<void> _setCached(
|
||||
TranslateActivityResponse activities,
|
||||
String l1,
|
||||
) async {
|
||||
final List<Future> 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<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
|
||||
static Map<String, DateTime> get sentFeedback {
|
||||
final entry = _storage.read("sent_feedback");
|
||||
if (entry != null && entry is Map<String, dynamic>) {
|
||||
try {
|
||||
return Map<String, DateTime>.from(
|
||||
entry.map((key, value) => MapEntry(key, DateTime.parse(value))),
|
||||
);
|
||||
} catch (e) {
|
||||
_storage.remove("sent_feedback");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static Future<void> 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<void> _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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
class TranslateActivityRequest {
|
||||
List<String> activityIds;
|
||||
String l1;
|
||||
|
||||
TranslateActivityRequest({
|
||||
required this.activityIds,
|
||||
required this.l1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"activity_ids": activityIds,
|
||||
"l1": l1,
|
||||
};
|
||||
|
||||
factory TranslateActivityRequest.fromJson(Map<String, dynamic> json) {
|
||||
return TranslateActivityRequest(
|
||||
activityIds: json['activity_ids'] != null
|
||||
? List<String>.from(json['activity_ids'])
|
||||
: [],
|
||||
l1: json['l1'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
|
||||
class TranslateActivityResponse {
|
||||
final Map<String, ActivityPlanModel> plans;
|
||||
|
||||
TranslateActivityResponse({required this.plans});
|
||||
|
||||
factory TranslateActivityResponse.fromJson(Map<String, dynamic> json) {
|
||||
final plansEntry = json['plans'] as Map<String, dynamic>;
|
||||
return TranslateActivityResponse(
|
||||
plans: plansEntry.map(
|
||||
(key, value) {
|
||||
return MapEntry(
|
||||
key,
|
||||
ActivityPlanModel.fromJson(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"plans": plans.map((key, value) => MapEntry(key, value.toJson())),
|
||||
};
|
||||
}
|
||||
|
|
@ -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<String, Completer<List<ActivityPlanModel>>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_activity_storage');
|
||||
|
||||
static Map<String, DateTime> get sentFeedback {
|
||||
final entry = _storage.read("sent_feedback");
|
||||
if (entry != null && entry is Map<String, dynamic>) {
|
||||
try {
|
||||
return Map<String, DateTime>.from(
|
||||
entry.map((key, value) => MapEntry(key, DateTime.parse(value))),
|
||||
);
|
||||
} catch (e) {
|
||||
_storage.remove("sent_feedback");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static Future<void> 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<void> _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<Map<String, dynamic>>(uuid);
|
||||
if (json != null) {
|
||||
try {
|
||||
return ActivityPlanModel.fromJson(json);
|
||||
} catch (e) {
|
||||
// ignore invalid cached data
|
||||
_storage.remove(uuid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _setCached(ActivityPlanModel activity) =>
|
||||
_storage.write(activity.activityId, activity.toJson());
|
||||
|
||||
static List<ActivityPlanModel> getSync(List<String> uuids) {
|
||||
return uuids
|
||||
.map((uuid) => _getCached(uuid))
|
||||
.whereType<ActivityPlanModel>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<List<ActivityPlanModel>> get(
|
||||
String topicId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
final activities = <ActivityPlanModel>[];
|
||||
final toFetch = <String>[];
|
||||
|
||||
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<List<ActivityPlanModel>> _fetch(
|
||||
String topicId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
if (_cache.containsKey(topicId)) {
|
||||
return _cache[topicId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<List<ActivityPlanModel>>();
|
||||
_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<Map<String, String>> _fetchImageUrls(
|
||||
List<CmsCoursePlanActivity> 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<String>().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<MapEntry<String, String>>(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
9
lib/pangea/course_plans/course_info_batch_request.dart
Normal file
9
lib/pangea/course_plans/course_info_batch_request.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class CourseInfoBatchRequest {
|
||||
final String batchId;
|
||||
final List<String> uuids;
|
||||
|
||||
CourseInfoBatchRequest({
|
||||
required this.batchId,
|
||||
required this.uuids,
|
||||
});
|
||||
}
|
||||
|
|
@ -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<String, Completer<Map<String, String>>> _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<void> _setCached(String uuid, String url) =>
|
||||
_storage.write(uuid, url);
|
||||
|
||||
static List<String> getSync(List<String> uuids) {
|
||||
final urls = <String>[];
|
||||
for (final uuid in uuids) {
|
||||
final cached = _getCached(uuid);
|
||||
if (cached != null) {
|
||||
urls.add(cached);
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
static Future<List<String>> get(String topicId, List<String> uuids) async {
|
||||
final urls = <String>[];
|
||||
final toFetch = <String>[];
|
||||
|
||||
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<Map<String, String>> _fetch(
|
||||
String topicId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
if (_cache.containsKey(topicId)) {
|
||||
return _cache[topicId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<Map<String, String>>();
|
||||
_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<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Completer<List<CourseLocationModel>>> _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<String, dynamic>.from(json));
|
||||
} catch (e) {
|
||||
// If parsing fails, remove the corrupted cache entry
|
||||
_storage.remove(uuid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _setCached(String uuid, CourseLocationModel location) =>
|
||||
_storage.write(uuid, location.toJson());
|
||||
|
||||
static List<CourseLocationModel> getSync(List<String> uuids) {
|
||||
final locations = <CourseLocationModel>[];
|
||||
for (final uuid in uuids) {
|
||||
final cached = _getCached(uuid);
|
||||
if (cached != null) {
|
||||
locations.add(cached);
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
static Future<List<CourseLocationModel>> get(
|
||||
String courseId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
final locations = <CourseLocationModel>[];
|
||||
final toFetch = <String>[];
|
||||
|
||||
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<List<CourseLocationModel>> _fetch(
|
||||
String topicId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
if (_cache.containsKey(topicId)) {
|
||||
return _cache[topicId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<List<CourseLocationModel>>();
|
||||
_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<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Completer<CourseLocationMediaResponse>> _cache = {};
|
||||
static final GetStorage _storage =
|
||||
GetStorage('course_location_media_storage');
|
||||
|
||||
static Future<CourseLocationMediaResponse> get(
|
||||
CourseInfoBatchRequest request,
|
||||
) async {
|
||||
await _storage.initStorage;
|
||||
final cached = getCached(request);
|
||||
final urls = List<CourseMediaInfo>.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<CourseLocationMediaResponse> _fetch(
|
||||
CourseInfoBatchRequest request,
|
||||
) async {
|
||||
if (_cache.containsKey(request.batchId)) {
|
||||
return _cache[request.batchId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<CourseLocationMediaResponse>();
|
||||
_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<CourseMediaInfo> 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<void> _setCached(CourseLocationMediaResponse response) async {
|
||||
final List<Future> futures = [];
|
||||
for (final entry in response.mediaUrls) {
|
||||
futures.add(_storage.write(entry.uuid, entry.url));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CourseMediaInfo> mediaUrls;
|
||||
|
||||
CourseLocationMediaResponse({
|
||||
required this.mediaUrls,
|
||||
});
|
||||
|
||||
factory CourseLocationMediaResponse.fromCmsResponse(
|
||||
PayloadPaginatedResponse<CmsCoursePlanTopicLocationMedia>
|
||||
cmsCoursePlanTopicLocationMediasResult,
|
||||
) {
|
||||
return CourseLocationMediaResponse(
|
||||
mediaUrls: cmsCoursePlanTopicLocationMediasResult.docs
|
||||
.where((e) => e.url != null)
|
||||
.map(
|
||||
(e) => CourseMediaInfo(
|
||||
uuid: e.id,
|
||||
url: e.url!,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Completer<CourseLocationResponse>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_location_storage');
|
||||
|
||||
static Future<CourseLocationResponse> 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<CourseLocationResponse> _fetch(
|
||||
CourseInfoBatchRequest request,
|
||||
) async {
|
||||
if (_cache.containsKey(request.batchId)) {
|
||||
return _cache[request.batchId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<CourseLocationResponse>();
|
||||
_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<CourseLocationModel> locations = [];
|
||||
for (final uuid in request.uuids) {
|
||||
final json = _storage.read(uuid);
|
||||
if (json != null) {
|
||||
try {
|
||||
final location =
|
||||
CourseLocationModel.fromJson(Map<String, dynamic>.from(json));
|
||||
locations.add(location);
|
||||
} catch (e) {
|
||||
// If parsing fails, remove the corrupted cache entry
|
||||
_storage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CourseLocationResponse(locations: locations);
|
||||
}
|
||||
|
||||
static Future<void> _setCached(CourseLocationResponse locations) {
|
||||
final List<Future> futures = [];
|
||||
for (final location in locations.locations) {
|
||||
futures.add(_storage.write(location.uuid, location.toJson()));
|
||||
}
|
||||
return Future.wait(futures);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CourseLocationModel> locations;
|
||||
|
||||
CourseLocationResponse({
|
||||
required this.locations,
|
||||
});
|
||||
|
||||
factory CourseLocationResponse.fromCmsResponse(
|
||||
PayloadPaginatedResponse<CmsCoursePlanTopicLocation> response,
|
||||
) {
|
||||
final locations = response.docs
|
||||
.map((location) => location.toCourseLocationModel())
|
||||
.toList();
|
||||
|
||||
return CourseLocationResponse(locations: locations);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
class CourseMediaInfo {
|
||||
final String uuid;
|
||||
final String url;
|
||||
|
||||
CourseMediaInfo({
|
||||
required this.uuid,
|
||||
required this.url,
|
||||
});
|
||||
}
|
||||
119
lib/pangea/course_plans/course_media/course_media_repo.dart
Normal file
119
lib/pangea/course_plans/course_media/course_media_repo.dart
Normal file
|
|
@ -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<String, Completer<CourseMediaResponse>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_media_storage');
|
||||
|
||||
static Future<CourseMediaResponse> get(
|
||||
CourseInfoBatchRequest request,
|
||||
) async {
|
||||
final urls = <CourseMediaInfo>[];
|
||||
|
||||
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<CourseMediaResponse> _fetch(
|
||||
CourseInfoBatchRequest request,
|
||||
) async {
|
||||
if (_cache.containsKey(request.batchId)) {
|
||||
return _cache[request.batchId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<CourseMediaResponse>();
|
||||
_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 = <CourseMediaInfo>[];
|
||||
|
||||
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<void> _setCached(CourseMediaResponse response) async {
|
||||
final List<Future> futures = [];
|
||||
for (final entry in response.mediaUrls) {
|
||||
futures.add(_storage.write(entry.uuid, entry.url));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CourseMediaInfo> mediaUrls;
|
||||
|
||||
CourseMediaResponse({
|
||||
required this.mediaUrls,
|
||||
});
|
||||
|
||||
factory CourseMediaResponse.fromCmsResponse(
|
||||
PayloadPaginatedResponse<CmsCoursePlanMedia> response,
|
||||
) {
|
||||
return CourseMediaResponse(
|
||||
mediaUrls: response.docs
|
||||
.where((e) => e.url != null)
|
||||
.map(
|
||||
(e) => CourseMediaInfo(
|
||||
uuid: e.id,
|
||||
url: e.url!,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Completer<Map<String, String>>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_media_storage');
|
||||
|
||||
static List<String> getSync(List<String> uuids) {
|
||||
final urls = <String>[];
|
||||
for (final uuid in uuids) {
|
||||
final cached = _getCached(uuid);
|
||||
if (cached != null) {
|
||||
urls.add(cached);
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
static Future<List<String>> get(String courseId, List<String> uuids) async {
|
||||
final urls = <String>[];
|
||||
final toFetch = <String>[];
|
||||
|
||||
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<void> _setCached(String uuid, String url) =>
|
||||
_storage.write(uuid, url);
|
||||
|
||||
static Future<Map<String, String>> _fetch(
|
||||
String courseId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
if (_cache.containsKey(courseId)) {
|
||||
return _cache[courseId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<Map<String, String>>();
|
||||
_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<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CoursePlanBuilder> createState() => CoursePlanController();
|
||||
}
|
||||
|
||||
class CoursePlanController extends State<CoursePlanBuilder> {
|
||||
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<void> _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<void> _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);
|
||||
}
|
||||
|
|
@ -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<String, Completer<CoursePlanModel>> 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<void> _setCached(CoursePlanModel coursePlan) async {
|
||||
if (lastUpdated == null) {
|
||||
await _courseStorage.write(
|
||||
"last_updated",
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
}
|
||||
await _courseStorage.write(coursePlan.uuid, coursePlan.toJson());
|
||||
}
|
||||
|
||||
static Future<CoursePlanModel> 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<CoursePlanModel>();
|
||||
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<List<CoursePlanModel>> search(
|
||||
List<String> ids, {
|
||||
Map<String, dynamic>? 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<CoursePlanModel>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<List<CoursePlanModel>> searchByFilter({
|
||||
CourseFilter? filter,
|
||||
}) async {
|
||||
await _courseStorage.initStorage;
|
||||
|
||||
final Map<String, dynamic> 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<void> clearCache() async {
|
||||
final List<Future> futures = [
|
||||
CourseActivityRepo.clearCache(),
|
||||
CourseLocationMediaRepo.clearCache(),
|
||||
CourseLocationRepo.clearCache(),
|
||||
CourseMediaRepo.clearCache(),
|
||||
CourseTopicRepo.clearCache(),
|
||||
_courseStorage.erase(),
|
||||
];
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> locationIds;
|
||||
final List<String> 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<CourseLocationModel> get loadedLocations =>
|
||||
CourseLocationRepo.getSync(locationIds);
|
||||
Future<List<CourseLocationModel>> 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<String> get loadedLocationMediaIds => loadedLocations
|
||||
.map((location) => CourseLocationMediaRepo.getSync(location.mediaIds))
|
||||
.expand((e) => e)
|
||||
.toList();
|
||||
Future<List<String>> fetchLocationMedia() async {
|
||||
final allLocationMedia = <String>[];
|
||||
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<ActivityPlanModel> get loadedActivities =>
|
||||
CourseActivityRepo.getSync(activityIds);
|
||||
Future<List<ActivityPlanModel>> fetchActivities() =>
|
||||
CourseActivityRepo.get(uuid, activityIds);
|
||||
|
||||
ActivityPlanModel? activityById(String activityId) =>
|
||||
loadedActivities.firstWhereOrNull(
|
||||
(activity) => activity.activityId == activityId,
|
||||
);
|
||||
|
||||
/// Deserialize from JSON
|
||||
factory CourseTopicModel.fromJson(Map<String, dynamic> json) {
|
||||
return CourseTopicModel(
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
uuid: json['uuid'] as String,
|
||||
activityIds: (json['activity_ids'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
locationIds: (json['location_ids'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'uuid': uuid,
|
||||
'activity_ids': activityIds,
|
||||
'location_ids': locationIds,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Completer<List<CourseTopicModel>>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_topic_storage');
|
||||
|
||||
static List<CourseTopicModel> getSync(List<String> uuids) {
|
||||
final topics = <CourseTopicModel>[];
|
||||
|
||||
for (final uuid in uuids) {
|
||||
final cached = _getCached(uuid);
|
||||
if (cached != null) {
|
||||
topics.add(cached);
|
||||
}
|
||||
}
|
||||
|
||||
return topics;
|
||||
}
|
||||
|
||||
static Future<List<CourseTopicModel>> get(
|
||||
String courseId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
final topics = <CourseTopicModel>[];
|
||||
final toFetch = <String>[];
|
||||
|
||||
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<String, dynamic>.from(json));
|
||||
} catch (e) {
|
||||
_storage.remove(uuid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _setCached(List<CourseTopicModel> topics) async {
|
||||
for (final topic in topics) {
|
||||
await _storage.write(topic.uuid, topic.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<CourseTopicModel>> _fetch(
|
||||
String courseId,
|
||||
List<String> uuids,
|
||||
) async {
|
||||
if (_cache.containsKey(courseId)) {
|
||||
return _cache[courseId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<List<CourseTopicModel>>();
|
||||
_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<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
137
lib/pangea/course_plans/course_topics/course_topic_model.dart
Normal file
137
lib/pangea/course_plans/course_topics/course_topic_model.dart
Normal file
|
|
@ -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<String> locationIds;
|
||||
final List<String> 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<CourseLocationResponse> 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<String> get loadedLocationMediaIds => loadedLocations.locations
|
||||
.map(
|
||||
(location) => CourseLocationMediaRepo.getCached(
|
||||
CourseInfoBatchRequest(
|
||||
batchId: uuid,
|
||||
uuids: location.mediaIds,
|
||||
),
|
||||
).mediaUrls,
|
||||
)
|
||||
.expand((e) => e)
|
||||
.map((e) => e.url)
|
||||
.toList();
|
||||
|
||||
Future<List<String>> fetchLocationMedia() async {
|
||||
final allLocationMedia = <String>[];
|
||||
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<String, ActivityPlanModel> get loadedActivities =>
|
||||
CourseActivityRepo.getCached(
|
||||
TranslateActivityRequest(
|
||||
activityIds: activityIds,
|
||||
l1: MatrixState.pangeaController.languageController.activeL1Code()!,
|
||||
),
|
||||
).plans;
|
||||
|
||||
Future<Map<String, ActivityPlanModel>> 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<String, dynamic> json) {
|
||||
final List<dynamic>? activityIdsEntry =
|
||||
json['activity_ids'] as List<dynamic>? ??
|
||||
json['activityIds'] as List<dynamic>?;
|
||||
final List<dynamic>? locationIdsEntry =
|
||||
json['location_ids'] as List<dynamic>? ??
|
||||
json['locationIds'] as List<dynamic>?;
|
||||
|
||||
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<String, dynamic> toJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'uuid': uuid,
|
||||
'activity_ids': activityIds,
|
||||
'location_ids': locationIds,
|
||||
};
|
||||
}
|
||||
}
|
||||
135
lib/pangea/course_plans/course_topics/course_topic_repo.dart
Normal file
135
lib/pangea/course_plans/course_topics/course_topic_repo.dart
Normal file
|
|
@ -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<String, Completer<TranslateTopicResponse>> _cache = {};
|
||||
static final GetStorage _storage = GetStorage('course_topic_storage');
|
||||
|
||||
static Future<TranslateTopicResponse> 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<TranslateTopicResponse> 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<TranslateTopicResponse> _fetch(
|
||||
TranslateTopicRequest request,
|
||||
String batchId,
|
||||
) async {
|
||||
if (_cache.containsKey(batchId)) {
|
||||
return _cache[batchId]!.future;
|
||||
}
|
||||
|
||||
final completer = Completer<TranslateTopicResponse>();
|
||||
_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<String, CourseTopicModel> 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<String, dynamic>.from(json),
|
||||
);
|
||||
topics[uuid] = topic;
|
||||
} catch (e) {
|
||||
_storage.remove(cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TranslateTopicResponse(topics: topics);
|
||||
}
|
||||
|
||||
static Future<void> _setCached(
|
||||
TranslateTopicResponse response,
|
||||
String l1,
|
||||
) async {
|
||||
final List<Future> futures = [];
|
||||
for (final entry in response.topics.entries) {
|
||||
futures.add(
|
||||
_storage.write(
|
||||
"${entry.key}_$l1",
|
||||
entry.value.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
await _storage.erase();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
class TranslateTopicRequest {
|
||||
List<String> topicIds;
|
||||
String l1;
|
||||
|
||||
TranslateTopicRequest({
|
||||
required this.topicIds,
|
||||
required this.l1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"topic_ids": topicIds,
|
||||
"l1": l1,
|
||||
};
|
||||
|
||||
factory TranslateTopicRequest.fromJson(Map<String, dynamic> json) {
|
||||
return TranslateTopicRequest(
|
||||
topicIds:
|
||||
json['topic_ids'] != null ? List<String>.from(json['topic_ids']) : [],
|
||||
l1: json['l1'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart';
|
||||
|
||||
class TranslateTopicResponse {
|
||||
final Map<String, CourseTopicModel> topics;
|
||||
|
||||
TranslateTopicResponse({required this.topics});
|
||||
|
||||
factory TranslateTopicResponse.fromJson(Map<String, dynamic> json) {
|
||||
final topicsEntry = json['topics'] as Map<String, dynamic>;
|
||||
return TranslateTopicResponse(
|
||||
topics: topicsEntry.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
CourseTopicModel.fromJson(value),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"topics": topics.map((key, value) => MapEntry(key, value.toJson())),
|
||||
};
|
||||
}
|
||||
78
lib/pangea/course_plans/courses/course_filter.dart
Normal file
78
lib/pangea/course_plans/courses/course_filter.dart
Normal file
|
|
@ -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<String, dynamic> get whereFilter {
|
||||
final Map<String, dynamic> 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;
|
||||
}
|
||||
102
lib/pangea/course_plans/courses/course_plan_builder.dart
Normal file
102
lib/pangea/course_plans/courses/course_plan_builder.dart
Normal file
|
|
@ -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<T extends StatefulWidget> on State<T> {
|
||||
bool loadingCourse = true;
|
||||
Object? courseError;
|
||||
|
||||
bool loadingTopics = false;
|
||||
Object? topicError;
|
||||
|
||||
Map<String, Object?> activityErrors = {};
|
||||
|
||||
CoursePlanModel? course;
|
||||
|
||||
Future<void> _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<void> 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<void> loadTopics() async {
|
||||
setState(() {
|
||||
loadingTopics = true;
|
||||
topicError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
if (course == null) {
|
||||
throw Exception("Course is null");
|
||||
}
|
||||
|
||||
final courseFutures = <Future>[
|
||||
course!.fetchMediaUrls(),
|
||||
course!.fetchTopics(),
|
||||
];
|
||||
await Future.wait(courseFutures);
|
||||
} catch (e) {
|
||||
topicError = e;
|
||||
} finally {
|
||||
if (mounted) setState(() => loadingTopics = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadActivity(String topicId) async {
|
||||
setState(() {
|
||||
activityErrors[topicId] = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final topic = course?.loadedTopics[topicId];
|
||||
if (topic == null) {
|
||||
throw Exception("Topic is null");
|
||||
}
|
||||
|
||||
final topicFutures = <Future>[];
|
||||
topicFutures.add(topic.fetchActivities());
|
||||
topicFutures.add(topic.fetchLocationMedia());
|
||||
await Future.wait(topicFutures);
|
||||
} catch (e) {
|
||||
activityErrors[topicId] = e;
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, dynamic> json) {
|
||||
return CoursePlanModel(
|
||||
|
|
@ -114,37 +95,42 @@ class CoursePlanModel {
|
|||
}
|
||||
|
||||
bool get topicListComplete => topicIds.length == loadedTopics.length;
|
||||
List<CourseTopicModel> get loadedTopics => CourseTopicRepo.getSync(topicIds);
|
||||
Future<List<CourseTopicModel>> fetchTopics() =>
|
||||
CourseTopicRepo.get(uuid, topicIds);
|
||||
|
||||
bool get mediaListComplete => mediaIds.length == loadedMediaUrls.length;
|
||||
List<String> get loadedMediaUrls => CourseMediaRepo.getSync(mediaIds);
|
||||
Future<List<String>> fetchMediaUrls() => CourseMediaRepo.get(uuid, mediaIds);
|
||||
String? get imageUrl => loadedMediaUrls.isEmpty
|
||||
? loadedTopics
|
||||
Map<String, CourseTopicModel> get loadedTopics => CourseTopicRepo.getCached(
|
||||
TranslateTopicRequest(
|
||||
topicIds: topicIds,
|
||||
l1: MatrixState.pangeaController.languageController.activeL1Code()!,
|
||||
),
|
||||
).topics;
|
||||
|
||||
Future<Map<String, CourseTopicModel>> 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<CourseMediaResponse> 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<void> init() async {
|
||||
final courseFutures = <Future>[
|
||||
fetchMediaUrls(),
|
||||
fetchTopics(),
|
||||
];
|
||||
await Future.wait(courseFutures);
|
||||
|
||||
final topicFutures = <Future>[];
|
||||
topicFutures.addAll(
|
||||
loadedTopics.map(
|
||||
(topic) => topic.fetchActivities(),
|
||||
),
|
||||
);
|
||||
topicFutures.addAll(
|
||||
loadedTopics.map(
|
||||
(topic) => topic.fetchLocationMedia(),
|
||||
),
|
||||
);
|
||||
await Future.wait(topicFutures);
|
||||
}
|
||||
: "${Environment.cmsApi}${loadedMediaUrls.mediaUrls.first}";
|
||||
}
|
||||
|
|
@ -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';
|
||||
266
lib/pangea/course_plans/courses/course_plans_repo.dart
Normal file
266
lib/pangea/course_plans/courses/course_plans_repo.dart
Normal file
|
|
@ -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<String, Completer<CoursePlanModel>> 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<CoursePlanModel> 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<CoursePlanModel>();
|
||||
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<GetLocalizedCoursesResponse> _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<GetLocalizedCoursesResponse> 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<GetLocalizedCoursesResponse> 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<String, CoursePlanModel> 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<String, dynamic>.from(json),
|
||||
);
|
||||
courses[uuid] = course;
|
||||
} catch (e) {
|
||||
_courseStorage.remove(cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetLocalizedCoursesResponse(coursePlans: courses);
|
||||
}
|
||||
|
||||
static Future<void> _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<void> _setCachedBatch(
|
||||
GetLocalizedCoursesResponse response,
|
||||
String l1,
|
||||
) async {
|
||||
if (lastUpdated == null) {
|
||||
await _courseStorage.write(
|
||||
"last_updated",
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
final List<Future> 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<void> clearCache() async {
|
||||
final List<Future> futures = [
|
||||
CourseActivityRepo.clearCache(),
|
||||
CourseLocationMediaRepo.clearCache(),
|
||||
CourseLocationRepo.clearCache(),
|
||||
CourseMediaRepo.clearCache(),
|
||||
CourseTopicRepo.clearCache(),
|
||||
_courseStorage.erase(),
|
||||
];
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
class GetLocalizedCoursesRequest {
|
||||
final List<String> coursePlanIds;
|
||||
final String l1;
|
||||
|
||||
GetLocalizedCoursesRequest({
|
||||
required this.coursePlanIds,
|
||||
required this.l1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"course_plan_ids": coursePlanIds,
|
||||
"l1": l1,
|
||||
};
|
||||
|
||||
factory GetLocalizedCoursesRequest.fromJson(Map<String, dynamic> json) {
|
||||
return GetLocalizedCoursesRequest(
|
||||
coursePlanIds: json['course_plan_ids'] != null
|
||||
? List<String>.from(json['course_plan_ids'])
|
||||
: [],
|
||||
l1: json['l1'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
|
||||
|
||||
class GetLocalizedCoursesResponse {
|
||||
final Map<String, CoursePlanModel> coursePlans;
|
||||
|
||||
GetLocalizedCoursesResponse({required this.coursePlans});
|
||||
|
||||
factory GetLocalizedCoursesResponse.fromJson(Map<String, dynamic> json) {
|
||||
final plansEntry = json['course_plans'] as Map<String, dynamic>;
|
||||
return GetLocalizedCoursesResponse(
|
||||
coursePlans: plansEntry.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
CoursePlanModel.fromJson(value),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"course_plans": coursePlans.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value.toJson(),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
@ -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<CourseSettings>
|
||||
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<void> _loadCourseInfo() async {
|
||||
setState(() => _loadingActivities = true);
|
||||
await loadCourse(widget.room.coursePlan!.uuid);
|
||||
if (course != null) {
|
||||
await loadTopics();
|
||||
|
||||
final futures = <Future>[];
|
||||
for (final topicId in course!.topicIds) {
|
||||
futures.add(loadActivity(topicId));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _loadingActivities = false);
|
||||
}
|
||||
|
||||
Future<void> _loadSummaries() async {
|
||||
|
|
@ -64,11 +92,11 @@ class CourseSettingsState extends State<CourseSettings>
|
|||
|
||||
@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<CourseSettings>
|
|||
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<String, List<User>> 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<ActivityPlanModel> activities;
|
||||
final Map<String, ActivityPlanModel> activities;
|
||||
final CourseSettingsState controller;
|
||||
|
||||
const TopicActivitiesList({
|
||||
|
|
@ -282,26 +333,33 @@ class TopicActivitiesListState extends State<TopicActivitiesList> {
|
|||
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<TopicActivitiesList> {
|
|||
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: [
|
||||
|
|
|
|||
|
|
@ -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<CreatePangeaAccountPage> {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class NewTripPageState extends State<NewTripPage> 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<NewTripPage> 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<NewTripPage> with CourseSearchProvider {
|
|||
],
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
courseId,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<PublicTripPage> {
|
|||
}
|
||||
|
||||
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<PublicTripPage> {
|
|||
}
|
||||
|
||||
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<PublicTripPage> {
|
|||
|
||||
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<PublicTripPage> {
|
|||
),
|
||||
if (course != null) ...[
|
||||
CourseInfoChips(
|
||||
course,
|
||||
courseId,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> 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<String, dynamic> 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, dynamic> json) {
|
||||
return CmsCoursePlanActivityRole(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
goal: json['goal'] as String,
|
||||
avatarUrl: json['avatarUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> json) {
|
||||
return CmsCoursePlanVocab(
|
||||
lemma: json['lemma'] as String,
|
||||
pos: json['pos'] as String,
|
||||
id: json['id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<CmsCoursePlanActivityRole> roles;
|
||||
final List<CmsCoursePlanVocab> vocabs;
|
||||
final JoinField? coursePlanActivityMedia;
|
||||
final List<String> 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<String, dynamic> 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<dynamic>)
|
||||
.map(
|
||||
(role) => CmsCoursePlanActivityRole.fromJson(
|
||||
role as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
vocabs: (json['vocabs'] as List<dynamic>)
|
||||
.map(
|
||||
(vocab) =>
|
||||
CmsCoursePlanVocab.fromJson(vocab as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
coursePlanActivityMedia:
|
||||
JoinField.fromJson(json['coursePlanActivityMedia']),
|
||||
coursePlanTopics: List<String>.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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<double>? coordinates;
|
||||
final JoinField? coursePlanActivityLocationMedia;
|
||||
final List<String> 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<String, dynamic> json) {
|
||||
return CmsCoursePlanActivityLocation(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
coordinates: (json['coordinates'] as List<dynamic>?)
|
||||
?.map((coord) => (coord as num).toDouble())
|
||||
.toList(),
|
||||
coursePlanActivityLocationMedia:
|
||||
json['coursePlanActivityLocationMedia'] != null
|
||||
? JoinField.fromJson(json['coursePlanActivityLocationMedia'])
|
||||
: null,
|
||||
coursePlanActivities: List<String>.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<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'coordinates': coordinates,
|
||||
'coursePlanActivities': coursePlanActivities,
|
||||
'coursePlanActivityLocationMedia':
|
||||
coursePlanActivityLocationMedia?.toJson(),
|
||||
'createdBy': createdBy?.toJson(),
|
||||
'updatedBy': updatedBy?.toJson(),
|
||||
'updatedAt': updatedAt,
|
||||
'createdAt': createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String, dynamic> json,
|
||||
) {
|
||||
return CmsCoursePlanActivityLocationMedia(
|
||||
id: json['id'],
|
||||
alt: json['alt'],
|
||||
coursePlanActivityLocations:
|
||||
List<String>.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<String, dynamic> 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String, dynamic> json) {
|
||||
return CmsCoursePlanActivityMedia(
|
||||
id: json['id'] as String,
|
||||
alt: json['alt'] as String?,
|
||||
coursePlanActivities: List<String>.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<String, dynamic> 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String, dynamic> 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<String>.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<String, dynamic> 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 ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'coursePlans': coursePlans?.toJson(),
|
||||
'createdBy': createdBy?.toJson(),
|
||||
'updatedBy': updatedBy?.toJson(),
|
||||
'updatedAt': updatedAt,
|
||||
'createdAt': createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue