feat: add ability to add course to existing space (#4037)
This commit is contained in:
parent
80080787b0
commit
1b3a97d8db
8 changed files with 286 additions and 169 deletions
|
|
@ -681,6 +681,31 @@ abstract class AppRoutes {
|
|||
routes: roomDetailsRoutes('spaceid'),
|
||||
),
|
||||
...roomDetailsRoutes('spaceid'),
|
||||
GoRoute(
|
||||
path: 'addcourse',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
NewCourse(
|
||||
spaceId: state.pathParameters['spaceid']!,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':courseId',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
SelectedCourse(
|
||||
state.pathParameters['courseId']!,
|
||||
spaceId: state.pathParameters['spaceid']!,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'activity/:activityid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
|
|
|
|||
|
|
@ -5100,7 +5100,7 @@
|
|||
"activityFinishedMessage": "All Finished!",
|
||||
"endForAll": "End for all",
|
||||
"newCourse": "New Course",
|
||||
"newCourseSubtitle": "Which course template would you like to use?",
|
||||
"newCourseSubtitle": "Which course plan plan would you like to use?",
|
||||
"failedToLoadCourses": "Failed to load courses",
|
||||
"numModules": "{num} modules",
|
||||
"@numModules": {
|
||||
|
|
@ -5134,7 +5134,7 @@
|
|||
"createGroupChatDesc": "Whereas activity sessions start and end, group chats will stay open for routine communication.",
|
||||
"deleteDesc": "Only admins can delete a course. This is a destructive action which removes all users and deletes all selected chats within the course. Proceed with caution.",
|
||||
"failedToLoadCourseInfo": "Failed to load course information",
|
||||
"noCourseFound": "No course information found",
|
||||
"noCourseFound": "Oh, this course needs a plan!\n\nCourse plans are a sequence of topics and conversation activities.",
|
||||
"additionalParticipants": "+ {num} others",
|
||||
"@additionalParticipants": {
|
||||
"type": "int",
|
||||
|
|
@ -5250,5 +5250,6 @@
|
|||
"pingSent": "🔔 Course ping sent! 🔔",
|
||||
"courseTitle": "Course title",
|
||||
"courseDesc": "Course description",
|
||||
"courseSavedSuccessfully": "Course saved successfully"
|
||||
"courseSavedSuccessfully": "Course saved successfully",
|
||||
"addCoursePlan": "Add a course plan"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
|
|||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
class NewCourse extends StatefulWidget {
|
||||
const NewCourse({super.key});
|
||||
final String? spaceId;
|
||||
const NewCourse({
|
||||
super.key,
|
||||
this.spaceId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NewCourse> createState() => NewCourseController();
|
||||
|
|
|
|||
|
|
@ -23,10 +23,15 @@ class NewCourseView extends StatelessWidget {
|
|||
const double descFontSize = 12.0;
|
||||
|
||||
const double iconSize = 12.0;
|
||||
final spaceId = controller.widget.spaceId;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).newCourse),
|
||||
title: Text(
|
||||
spaceId != null
|
||||
? L10n.of(context).addCoursePlan
|
||||
: L10n.of(context).newCourse,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -41,6 +46,7 @@ class NewCourseView extends StatelessWidget {
|
|||
L10n.of(context).newCourseSubtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
|
@ -125,7 +131,9 @@ class NewCourseView extends StatelessWidget {
|
|||
child: CoursePlanTile(
|
||||
course: controller.courses[index],
|
||||
onTap: () => context.go(
|
||||
"/rooms/communities/newcourse/${controller.courses[index].uuid}",
|
||||
spaceId != null
|
||||
? "/rooms/spaces/$spaceId/addcourse/${controller.courses[index].uuid}"
|
||||
: "/rooms/communities/newcourse/${controller.courses[index].uuid}",
|
||||
),
|
||||
titleFontSize: titleFontSize,
|
||||
chipFontSize: descFontSize,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,15 @@ import 'package:matrix/matrix.dart' as sdk;
|
|||
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/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SelectedCourse extends StatefulWidget {
|
||||
final String courseId;
|
||||
const SelectedCourse(this.courseId, {super.key});
|
||||
final String? spaceId;
|
||||
const SelectedCourse(this.courseId, {super.key, this.spaceId});
|
||||
|
||||
@override
|
||||
SelectedCourseController createState() => SelectedCourseController();
|
||||
|
|
@ -70,9 +72,28 @@ class SelectedCourseController extends State<SelectedCourse> {
|
|||
context.go("/rooms/spaces/${room.id}/details");
|
||||
}
|
||||
|
||||
Future<void> addCourseToSpace(CoursePlanModel course) async {
|
||||
if (widget.spaceId == null) return;
|
||||
final space = Matrix.of(context).client.getRoomById(widget.spaceId!);
|
||||
|
||||
if (space == null) {
|
||||
throw Exception("Space not found");
|
||||
}
|
||||
|
||||
await space.addCourseToSpace(widget.courseId);
|
||||
|
||||
if (space.name.isEmpty) {
|
||||
await space.setName(course.title);
|
||||
}
|
||||
|
||||
if (space.topic.isEmpty) {
|
||||
await space.setDescription(course.description);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
context.go("/rooms/spaces/${space.id}/details");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SelectedCourseView(
|
||||
courseId: widget.courseId,
|
||||
launchCourse: launchCourse,
|
||||
);
|
||||
Widget build(BuildContext context) => SelectedCourseView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,18 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:fluffychat/l10n/l10n.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/course_plan_model.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';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class SelectedCourseView extends StatelessWidget {
|
||||
final String courseId;
|
||||
final Future<void> Function(CoursePlanModel course) launchCourse;
|
||||
const SelectedCourseView({
|
||||
final SelectedCourseController controller;
|
||||
const SelectedCourseView(
|
||||
this.controller, {
|
||||
super.key,
|
||||
required this.courseId,
|
||||
required this.launchCourse,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -32,171 +30,182 @@ class SelectedCourseView extends StatelessWidget {
|
|||
const double mediumIconSize = 16.0;
|
||||
const double smallIconSize = 12.0;
|
||||
|
||||
final spaceId = controller.widget.spaceId;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).newCourse),
|
||||
title: Text(
|
||||
spaceId != null
|
||||
? L10n.of(context).addCoursePlan
|
||||
: L10n.of(context).newCourse,
|
||||
),
|
||||
),
|
||||
body: CoursePlanBuilder(
|
||||
courseId: courseId,
|
||||
courseId: controller.widget.courseId,
|
||||
onNotFound: () => context.go("/rooms/communities/newcourse"),
|
||||
builder: (context, controller) {
|
||||
final course = controller.course;
|
||||
builder: (context, courseController) {
|
||||
final course = courseController.course;
|
||||
return MaxWidthBody(
|
||||
showBorder: false,
|
||||
withScrolling: false,
|
||||
maxWidth: 500.0,
|
||||
child: course == null
|
||||
? const Center(child: CircularProgressIndicator.adaptive())
|
||||
: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ListView.builder(
|
||||
itemCount: course.loadedTopics.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: MapClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
width: 100.0,
|
||||
borderRadius: BorderRadius.circular(0.0),
|
||||
replacement: Container(
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ListView.builder(
|
||||
itemCount: course.loadedTopics.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: MapClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
borderRadius:
|
||||
BorderRadius.circular(0.0),
|
||||
replacement: Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
course.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
Text(
|
||||
course.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style:
|
||||
const TextStyle(fontSize: descFontSize),
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
bottom: 8.0,
|
||||
Text(
|
||||
course.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.map,
|
||||
size: largeIconSize,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).coursePlan,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
CourseInfoChips(
|
||||
course,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.map,
|
||||
size: largeIconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
L10n.of(context).coursePlan,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
index--;
|
||||
index--;
|
||||
|
||||
if (index == course.loadedTopics.length) {
|
||||
return const SizedBox(height: 150.0);
|
||||
}
|
||||
if (index == course.loadedTopics.length) {
|
||||
return const SizedBox(height: 150.0);
|
||||
}
|
||||
|
||||
final topic = course.loadedTopics[index];
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: PinClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: topic.imageUrl,
|
||||
width: 45.0,
|
||||
replacement: Container(
|
||||
final topic = course.loadedTopics[index];
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: PinClipper(),
|
||||
child: ImageByUrl(
|
||||
imageUrl: topic.imageUrl,
|
||||
width: 45.0,
|
||||
height: 45.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
replacement: Container(
|
||||
width: 45.0,
|
||||
height: 45.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
topic.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
Text(
|
||||
topic.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry
|
||||
.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (topic.location != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry
|
||||
.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (topic.location != null)
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: topic.location!,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: topic.location!,
|
||||
icon:
|
||||
Icons.event_note_outlined,
|
||||
text: L10n.of(context)
|
||||
.numActivityPlans(
|
||||
topic.loadedActivities
|
||||
.length,
|
||||
),
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.event_note_outlined,
|
||||
text: L10n.of(context)
|
||||
.numActivityPlans(
|
||||
topic.loadedActivities.length,
|
||||
),
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
|
|
@ -246,29 +255,36 @@ class SelectedCourseView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 16.0,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => launchCourse(course),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).createCourse,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => spaceId != null
|
||||
? controller.addCourseToSpace(course)
|
||||
: controller.launchCourse(course),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
Text(
|
||||
spaceId != null
|
||||
? L10n.of(context).addCoursePlan
|
||||
: L10n.of(context).createCourse,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -147,6 +147,17 @@ extension CoursePlanRoomExtension on Room {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<void> addCourseToSpace(String courseId) async {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.coursePlan,
|
||||
"",
|
||||
{
|
||||
"uuid": courseId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, List<User>>> topicsToUsers(CoursePlanModel course) async {
|
||||
final Map<String, List<User>> topicUserMap = {};
|
||||
final users = await requestParticipants(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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_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 StatelessWidget {
|
||||
final Room room;
|
||||
|
|
@ -41,7 +42,37 @@ class CourseSettings extends StatelessWidget {
|
|||
}
|
||||
|
||||
if (controller.course == null) {
|
||||
return Center(child: Text(L10n.of(context).noCourseFound));
|
||||
return room.canChangeStateEvent(PangeaEventTypes.coursePlan)
|
||||
? Column(
|
||||
spacing: 50.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).noCourseFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () =>
|
||||
context.go("/rooms/spaces/${room.id}/addcourse"),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined),
|
||||
Text(L10n.of(context).addCoursePlan),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(child: Text(L10n.of(context).noCourseFound));
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue