From b7b55226490219be0b9c50a9cadc9a58eacf5fc2 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:57:07 -0400 Subject: [PATCH] 2223 make your own activity page (#2245) * feat: added make your own activity page * chore: center content of activity planner page --- assets/l10n/intl_en.arb | 8 +- lib/config/routes.dart | 10 + .../activity_generator.dart | 233 ++++++++++++++++ .../activity_generator_view.dart | 253 ++++++++++++++++++ .../activity_planner/activity_plan_card.dart | 51 +++- .../activity_planner_page.dart | 242 ++++------------- .../activity_planner_page_appbar.dart | 48 ++-- .../generated_activity_list.dart | 151 ----------- .../activity_planner/new_activity_form.dart | 193 ------------- .../activity_suggestions_area.dart | 35 ++- .../activity_suggestions_constants.dart | 2 + .../make_activity_card.dart | 70 +++++ 12 files changed, 731 insertions(+), 565 deletions(-) create mode 100644 lib/pangea/activity_generator/activity_generator.dart create mode 100644 lib/pangea/activity_generator/activity_generator_view.dart delete mode 100644 lib/pangea/activity_planner/generated_activity_list.dart delete mode 100644 lib/pangea/activity_planner/new_activity_form.dart create mode 100644 lib/pangea/activity_suggestions/make_activity_card.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 857cdb99e..2e6c825a1 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4836,5 +4836,11 @@ "selectForGrammar": "Select a grammar icon for activities and details.", "newChatActivityTitle": "Add a fun activity", "newChatActivityDesc": "Make every group chat an adventure with Activity Planner! Set captivating topics and objectives for the group, and bring conversations to life with stunning images. Spark imaginative discussions and keep the fun flowing effortlessly!", - "exploreMore": "Explore more" + "exploreMore": "Explore more", + "randomize": "Randomize", + "clear": "Clear", + "makeYourOwnActivity": "Make your own activity", + "makeYourOwn": "Make your own", + "featuredActivities": "Featured activities", + "yourBookmarks": "Your bookmarks" } \ No newline at end of file diff --git a/lib/config/routes.dart b/lib/config/routes.dart index a705b0149..514384024 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -29,6 +29,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d import 'package:fluffychat/pages/settings_password/settings_password.dart'; import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; +import 'package:fluffychat/pangea/activity_generator/activity_generator.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; @@ -255,6 +256,15 @@ abstract class AppRoutes { const SuggestionsPage(), ), ), + GoRoute( + path: '/planner', + redirect: loggedOutRedirect, + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const ActivityGenerator(), + ), + ), // Pangea# GoRoute( path: 'archive', diff --git a/lib/pangea/activity_generator/activity_generator.dart b/lib/pangea/activity_generator/activity_generator.dart new file mode 100644 index 000000000..9366ee01c --- /dev/null +++ b/lib/pangea/activity_generator/activity_generator.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/activity_generator/activity_generator_view.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.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/activity_planner/activity_plan_response.dart'; +import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart'; +import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; +import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; +import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ActivityGenerator extends StatefulWidget { + const ActivityGenerator({super.key}); + + @override + ActivityGeneratorState createState() => ActivityGeneratorState(); +} + +class ActivityGeneratorState extends State { + bool loading = false; + String? error; + List? activities; + + final formKey = GlobalKey(); + + final topicController = TextEditingController(); + final objectiveController = TextEditingController(); + final modeController = TextEditingController(); + + MediaEnum selectedMedia = MediaEnum.nan; + String? selectedLanguageOfInstructions; + String? selectedTargetLanguage; + LanguageLevelTypeEnum? selectedCefrLevel; + int? selectedNumberOfParticipants; + + String? avatarURL; + String? filename; + + @override + void initState() { + super.initState(); + selectedLanguageOfInstructions = + MatrixState.pangeaController.languageController.userL1?.langCode; + selectedTargetLanguage = + MatrixState.pangeaController.languageController.userL2?.langCode; + selectedCefrLevel = LanguageLevelTypeEnum.a1; + selectedNumberOfParticipants = 3; + _setModeImageURL(); + } + + @override + void dispose() { + topicController.dispose(); + objectiveController.dispose(); + modeController.dispose(); + super.dispose(); + } + + ActivitySettingRequestSchema get req => ActivitySettingRequestSchema( + langCode: + MatrixState.pangeaController.languageController.userL2?.langCode ?? + LanguageKeys.defaultLanguage, + ); + + ActivityPlanRequest get planRequest => ActivityPlanRequest( + topic: topicController.text, + mode: modeController.text, + objective: objectiveController.text, + media: selectedMedia, + languageOfInstructions: selectedLanguageOfInstructions!, + targetLanguage: selectedTargetLanguage!, + cefrLevel: selectedCefrLevel!, + numberOfParticipants: selectedNumberOfParticipants!, + ); + + Future> get topicItems => + TopicListRepo.get(req); + + Future> get modeItems => + ActivityModeListRepo.get(req); + + Future> get objectiveItems => + LearningObjectiveListRepo.get(req); + + String? validateNotNull(String? value) { + if (value == null || value.isEmpty) { + return L10n.of(context).interactiveTranslatorRequired; + } + return null; + } + + Future _randomTopic() async { + final topics = await topicItems; + return (topics..shuffle()).first.name; + } + + Future _randomObjective() async { + final objectives = await objectiveItems; + return (objectives..shuffle()).first.name; + } + + Future _randomMode() async { + final modes = await modeItems; + return (modes..shuffle()).first.name; + } + + void randomizeSelections() async { + final selectedTopic = await _randomTopic(); + final selectedObjective = await _randomObjective(); + final selectedMode = await _randomMode(); + + setState(() { + topicController.text = selectedTopic; + objectiveController.text = selectedObjective; + modeController.text = selectedMode; + }); + } + + void clearSelections() async { + setState(() { + topicController.clear(); + objectiveController.clear(); + modeController.clear(); + selectedMedia = MediaEnum.nan; + selectedLanguageOfInstructions = + MatrixState.pangeaController.languageController.userL1?.langCode; + selectedTargetLanguage = + MatrixState.pangeaController.languageController.userL2?.langCode; + selectedCefrLevel = LanguageLevelTypeEnum.a1; + selectedNumberOfParticipants = 3; + }); + } + + void setSelectedNumberOfParticipants(int? value) { + setState(() => selectedNumberOfParticipants = value); + } + + void setSelectedTargetLanguage(String? value) { + setState(() => selectedTargetLanguage = value); + } + + void setSelectedLanguageOfInstructions(String? value) { + setState(() => selectedLanguageOfInstructions = value); + } + + void setSelectedCefrLevel(LanguageLevelTypeEnum? value) { + setState(() => selectedCefrLevel = value); + } + + void setSelectedMedia(MediaEnum? value) { + if (value == null) return; + setState(() => selectedMedia = value); + } + + Future get _selectedMode async { + final modes = await modeItems; + return modes.firstWhereOrNull( + (element) => element.name.toLowerCase() == planRequest.mode.toLowerCase(), + ); + } + + Future _setModeImageURL() async { + final mode = await _selectedMode; + if (mode == null) return; + + final modeName = + mode.defaultName.toLowerCase().replaceAll(RegExp(r'\s+'), ''); + + if (!mounted) return; + setState(() { + filename = + "${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg"; + avatarURL = "${AppConfig.assetsBaseURL}/$filename"; + }); + } + + Future onEdit(int index, ActivityPlanModel updatedActivity) async { + // in this case we're editing an activity plan that was generated recently + // via the repo and should be updated in the cached response + if (activities != null) { + activities?[index] = updatedActivity; + ActivityPlanGenerationRepo.set( + planRequest, + ActivityPlanResponse(activityPlans: activities!), + ); + } + + setState(() {}); + } + + void update() => setState(() {}); + + Future generate() async { + setState(() { + loading = true; + error = null; + }); + + try { + await _setModeImageURL(); + final resp = await ActivityPlanGenerationRepo.get(planRequest); + for (final activity in resp.activityPlans) { + activity.imageURL = avatarURL; + } + activities = resp.activityPlans; + } catch (e, s) { + error = e.toString(); + ErrorHandler.logError( + e: e, + s: s, + data: { + 'activityPlanRequest': planRequest, + }, + ); + } finally { + if (mounted) setState(() => loading = false); + } + } + + @override + Widget build(BuildContext context) => ActivityGeneratorView(this); +} diff --git a/lib/pangea/activity_generator/activity_generator_view.dart b/lib/pangea/activity_generator/activity_generator_view.dart new file mode 100644 index 000000000..b10ad743a --- /dev/null +++ b/lib/pangea/activity_generator/activity_generator_view.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/activity_generator/activity_generator.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_card.dart'; +import 'package:fluffychat/pangea/activity_planner/suggestion_form_field.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; +import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; +import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ActivityGeneratorView extends StatelessWidget { + final ActivityGeneratorState controller; + + const ActivityGeneratorView( + this.controller, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final l10n = L10n.of(context); + Widget? body; + + if (controller.loading) { + body = const Padding( + padding: EdgeInsets.all(32.0), + child: Center(child: CircularProgressIndicator()), + ); + } else if (controller.error != null) { + body = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(l10n.oopsSomethingWentWrong), + const SizedBox(height: 16), + ElevatedButton( + onPressed: controller.generate, + child: Text(l10n.tryAgain), + ), + ], + ), + ); + } else if (controller.activities != null && + controller.activities!.isNotEmpty) { + body = ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: controller.activities!.length, + itemBuilder: (context, index) { + return ActivityPlanCard( + activity: controller.activities![index], + room: null, + onEdit: (updatedActivity) => + controller.onEdit(index, updatedActivity), + onChange: controller.update, + avatarURL: controller.avatarURL, + initialFilename: controller.filename, + ); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).makeYourOwnActivity), + ), + body: body ?? + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: controller.formKey, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + const InstructionsInlineTooltip( + instructionsEnum: + InstructionsEnum.activityPlannerOverview, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: ClipRRect( + child: CachedNetworkImage( + fit: BoxFit.cover, + imageUrl: + "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.makeActivityAssetPath}", + placeholder: (context, url) { + return const Center( + child: CircularProgressIndicator(), + ); + }, + errorWidget: (context, url, error) => + const SizedBox(), + ), + ), + ), + const SizedBox(height: 16.0), + PLanguageDropdown( + languages: MatrixState + .pangeaController.pLanguageStore.baseOptions, + onChange: (val) => controller + .setSelectedLanguageOfInstructions(val.langCode), + initialLanguage: + controller.selectedLanguageOfInstructions != null + ? PLanguageStore.byLangCode( + controller.selectedLanguageOfInstructions!, + ) + : MatrixState + .pangeaController.languageController.userL1, + isL2List: false, + decorationText: + L10n.of(context).languageOfInstructionsLabel, + ), + const SizedBox(height: 16.0), + PLanguageDropdown( + languages: MatrixState + .pangeaController.pLanguageStore.targetOptions, + onChange: (val) => + controller.setSelectedTargetLanguage(val.langCode), + initialLanguage: controller.selectedTargetLanguage != null + ? PLanguageStore.byLangCode( + controller.selectedTargetLanguage!, + ) + : MatrixState + .pangeaController.languageController.userL2, + decorationText: L10n.of(context).targetLanguageLabel, + isL2List: true, + ), + const SizedBox(height: 16.0), + SuggestionFormField( + suggestions: controller.topicItems, + validator: controller.validateNotNull, + label: l10n.topicLabel, + placeholder: l10n.topicPlaceholder, + controller: controller.topicController, + ), + const SizedBox(height: 16.0), + SuggestionFormField( + suggestions: controller.objectiveItems, + validator: controller.validateNotNull, + label: l10n.learningObjectiveLabel, + placeholder: l10n.learningObjectivePlaceholder, + controller: controller.objectiveController, + ), + const SizedBox(height: 16.0), + SuggestionFormField( + suggestions: controller.modeItems, + validator: controller.validateNotNull, + label: l10n.modeLabel, + placeholder: l10n.modePlaceholder, + controller: controller.modeController, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + labelText: l10n.numberOfLearners, + ), + textInputAction: TextInputAction.done, + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.mustBeInteger; + } + final n = int.tryParse(value); + if (n == null || n <= 0) { + return l10n.mustBeInteger; + } + return null; + }, + onChanged: (val) => controller + .setSelectedNumberOfParticipants(int.tryParse(val)), + initialValue: + controller.selectedNumberOfParticipants?.toString(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + onFieldSubmitted: (_) { + if (controller.formKey.currentState?.validate() ?? + false) { + controller.generate(); + } + }, + ), + const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ElevatedButton( + onPressed: controller.clearSelections, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.reset_focus), + const SizedBox(width: 8), + Text(L10n.of(context).clear), + ], + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ElevatedButton( + onPressed: controller.randomizeSelections, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.shuffle), + const SizedBox(width: 8), + Text(L10n.of(context).randomize), + ], + ), + ), + ), + ), + ], + ), + const SizedBox(height: 24.0), + ElevatedButton( + onPressed: () { + if (controller.formKey.currentState?.validate() ?? + false) { + controller.generate(); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.lightbulb_outline), + const SizedBox(width: 8), + Text(l10n.generateActivitiesButton), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index 5cad6cc5a..a96bba4e8 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -5,16 +5,20 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; +import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class ActivityPlanCard extends StatefulWidget { final ActivityPlanModel activity; @@ -149,9 +153,11 @@ class ActivityPlanCardState extends State { }); } - Future _onLaunch() => showFutureLoadingDialog( - context: context, - future: () async { + Future _onLaunch() async { + await showFutureLoadingDialog( + context: context, + future: () async { + if (widget.room != null) { await widget.room?.sendActivityPlan( widget.activity, avatar: _avatar, @@ -160,8 +166,43 @@ class ActivityPlanCardState extends State { ); Navigator.of(context).pop(); - }, - ); + return; + } + + final client = Matrix.of(context).client; + final roomId = await client.createGroupChat( + preset: CreateRoomPreset.publicChat, + visibility: sdk.Visibility.private, + groupName: + widget.activity.title.isNotEmpty ? widget.activity.title : null, + initialState: [ + StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: defaultPowerLevels(client.userID!), + ), + ], + enableEncryption: false, + ); + + Room? room = client.getRoomById(roomId); + if (room == null) { + await client.waitForRoomInSync(roomId); + room = client.getRoomById(roomId); + } + if (room == null) return; + + await room.sendActivityPlan( + widget.activity, + avatar: _avatar, + avatarURL: _avatarURL, + filename: _filename, + ); + + context.go("/rooms/$roomId"); + }, + ); + } bool get isBookmarked => BookmarkedActivitiesRepo.isBookmarked(widget.activity); diff --git a/lib/pangea/activity_planner/activity_planner_page.dart b/lib/pangea/activity_planner/activity_planner_page.dart index 2346d147b..946c58551 100644 --- a/lib/pangea/activity_planner/activity_planner_page.dart +++ b/lib/pangea/activity_planner/activity_planner_page.dart @@ -1,30 +1,16 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_page_appbar.dart'; import 'package:fluffychat/pangea/activity_planner/bookmarked_activity_list.dart'; -import 'package:fluffychat/pangea/activity_planner/generated_activity_list.dart'; -import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart'; -import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; -import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; -import 'package:fluffychat/pangea/activity_planner/new_activity_form.dart'; -import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum PageMode { - settings, - generatedActivities, featuredActivities, savedActivities, } @@ -38,164 +24,18 @@ class ActivityPlannerPage extends StatefulWidget { } class ActivityPlannerPageState extends State { - final formKey = GlobalKey(); PageMode pageMode = PageMode.featuredActivities; - - MediaEnum selectedMedia = MediaEnum.nan; - String? selectedLanguageOfInstructions; - String? selectedTargetLanguage; - LanguageLevelTypeEnum? selectedCefrLevel; - int? selectedNumberOfParticipants; - - List activities = []; - Room? get room => Matrix.of(context).client.getRoomById(widget.roomID); - ActivityPlanModel? get _initialActivity => room?.activityPlan; - - @override - void initState() { - super.initState(); - if (room == null) { - Navigator.of(context).pop(); - return; - } - - if (_initialActivity == null) { - selectedLanguageOfInstructions = - MatrixState.pangeaController.languageController.userL1?.langCode; - selectedTargetLanguage = - MatrixState.pangeaController.languageController.userL2?.langCode; - selectedCefrLevel = LanguageLevelTypeEnum.a1; - selectedNumberOfParticipants = - max(room?.getParticipants().length ?? 1, 1); - } else { - selectedMedia = _initialActivity!.req.media; - selectedLanguageOfInstructions = - _initialActivity!.req.languageOfInstructions; - selectedTargetLanguage = _initialActivity!.req.targetLanguage; - selectedCefrLevel = _initialActivity!.req.cefrLevel; - selectedNumberOfParticipants = _initialActivity!.req.numberOfParticipants; - topicController.text = _initialActivity!.req.topic; - objectiveController.text = _initialActivity!.req.objective; - modeController.text = _initialActivity!.req.mode; - } - } - - final topicController = TextEditingController(); - final objectiveController = TextEditingController(); - final modeController = TextEditingController(); - - @override - void dispose() { - topicController.dispose(); - objectiveController.dispose(); - modeController.dispose(); - super.dispose(); - } - - ActivitySettingRequestSchema get req => ActivitySettingRequestSchema( - langCode: - MatrixState.pangeaController.languageController.userL2?.langCode ?? - LanguageKeys.defaultLanguage, - ); - - Future> get topicItems => - TopicListRepo.get(req); - - Future> get modeItems => - ActivityModeListRepo.get(req); - - Future> get objectiveItems => - LearningObjectiveListRepo.get(req); - void _setPageMode(PageMode? mode) { if (mode == null) return; setState(() => pageMode = mode); } - void setSelectedNumberOfParticipants(int? value) { - setState(() => selectedNumberOfParticipants = value); - } - - void setSelectedTargetLanguage(String? value) { - setState(() => selectedTargetLanguage = value); - } - - void setSelectedLanguageOfInstructions(String? value) { - setState(() => selectedLanguageOfInstructions = value); - } - - void setSelectedCefrLevel(LanguageLevelTypeEnum? value) { - setState(() => selectedCefrLevel = value); - } - - void setSelectedMedia(MediaEnum? value) { - if (value == null) return; - setState(() => selectedMedia = value); - } - - Future generateActivities() async => - _setPageMode(PageMode.generatedActivities); - - Future _randomTopic() async { - final topics = await topicItems; - return (topics..shuffle()).first.name; - } - - Future _randomObjective() async { - final objectives = await objectiveItems; - return (objectives..shuffle()).first.name; - } - - Future _randomMode() async { - final modes = await modeItems; - return (modes..shuffle()).first.name; - } - - void randomizeSelections() async { - final selectedTopic = await _randomTopic(); - final selectedObjective = await _randomObjective(); - final selectedMode = await _randomMode(); - - setState(() { - topicController.text = selectedTopic; - objectiveController.text = selectedObjective; - modeController.text = selectedMode; - }); - } - - // Add validation logic - String? validateNotNull(String? value) { - if (value == null || value.isEmpty) { - return L10n.of(context).interactiveTranslatorRequired; - } - return null; - } - - ActivityPlanRequest get planRequest => ActivityPlanRequest( - topic: topicController.text, - mode: modeController.text, - objective: objectiveController.text, - media: selectedMedia, - languageOfInstructions: selectedLanguageOfInstructions!, - targetLanguage: selectedTargetLanguage!, - cefrLevel: selectedCefrLevel!, - numberOfParticipants: selectedNumberOfParticipants!, - ); - @override Widget build(BuildContext context) { Widget body = const SizedBox(); switch (pageMode) { - case PageMode.settings: - body = NewActivityForm(this); - break; - case PageMode.generatedActivities: - body = GeneratedActivitiesList( - controller: this, - ); - break; case PageMode.savedActivities: body = BookmarkedActivitiesList( room: room, @@ -207,6 +47,7 @@ class ActivityPlannerPageState extends State { child: SingleChildScrollView( child: ActivitySuggestionsArea( scrollDirection: Axis.vertical, + includeCustomCards: false, ), ), ); @@ -218,36 +59,61 @@ class ActivityPlannerPageState extends State { pageMode: pageMode, setPageMode: _setPageMode, ), - body: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800.0), - child: Column( - children: [ - if ([PageMode.featuredActivities, PageMode.savedActivities] - .contains(pageMode)) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: SegmentedButton( - selected: {pageMode}, - onSelectionChanged: (modes) => _setPageMode(modes.first), - segments: const [ - ButtonSegment( - value: PageMode.featuredActivities, - label: Text("Featured activities"), - ), - ButtonSegment( - value: PageMode.savedActivities, - label: Text("Your bookmarks"), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800.0), + child: Column( + children: [ + if ([PageMode.featuredActivities, PageMode.savedActivities] + .contains(pageMode)) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SegmentedButton( + selected: {pageMode}, + onSelectionChanged: (modes) => + _setPageMode(modes.first), + segments: [ + ButtonSegment( + value: PageMode.featuredActivities, + label: Text(L10n.of(context).featuredActivities), + ), + ButtonSegment( + value: PageMode.savedActivities, + label: Text(L10n.of(context).yourBookmarks), + ), + ], + ), + ), + ], + ), + body, + if (!FluffyThemes.isColumnMode(context)) + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: () => context.go("/rooms/planner"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 0.0, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context).makeYourOwn, + style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), - ], - ), - body, - ], + ), + ], + ), ), ), ); diff --git a/lib/pangea/activity_planner/activity_planner_page_appbar.dart b/lib/pangea/activity_planner/activity_planner_page_appbar.dart index 0e79dff9d..7543fe9b7 100644 --- a/lib/pangea/activity_planner/activity_planner_page_appbar.dart +++ b/lib/pangea/activity_planner/activity_planner_page_appbar.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; class ActivityPlannerPageAppBar extends StatelessWidget @@ -21,20 +23,15 @@ class ActivityPlannerPageAppBar extends StatelessWidget Widget build(BuildContext context) { final l10n = L10n.of(context); return AppBar( - leading: pageMode != PageMode.settings && - pageMode != PageMode.generatedActivities - ? IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ) - : IconButton( - onPressed: () => setPageMode( - pageMode == PageMode.settings - ? PageMode.featuredActivities - : PageMode.settings, - ), - icon: const Icon(Icons.arrow_back), - ), + leadingWidth: FluffyThemes.isColumnMode(context) ? 150.0 : 50.0, + leading: Container( + padding: const EdgeInsets.only(left: 16.0), + alignment: Alignment.centerLeft, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ), title: pageMode == PageMode.savedActivities ? Center( child: Row( @@ -64,10 +61,25 @@ class ActivityPlannerPageAppBar extends StatelessWidget ), ), actions: [ - IconButton( - onPressed: () => setPageMode(PageMode.settings), - icon: const Icon(Icons.edit_outlined), - ), + FluffyThemes.isColumnMode(context) + ? Container( + width: 150.0, + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () => context.go("/rooms/planner"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 0.0, + ), + ), + child: Text( + L10n.of(context).makeYourOwn, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ) + : const SizedBox(width: 50.0), ], ); } diff --git a/lib/pangea/activity_planner/generated_activity_list.dart b/lib/pangea/activity_planner/generated_activity_list.dart deleted file mode 100644 index f5f746ebe..000000000 --- a/lib/pangea/activity_planner/generated_activity_list.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; -import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'activity_plan_card.dart'; - -class GeneratedActivitiesList extends StatefulWidget { - final ActivityPlannerPageState controller; - - const GeneratedActivitiesList({ - super.key, - required this.controller, - }); - - @override - GeneratedActivitiesListState createState() => GeneratedActivitiesListState(); -} - -class GeneratedActivitiesListState extends State { - List? _activities; - bool _isLoading = true; - Object? _error; - - String? _avatarURL; - String? _filename; - - @override - void initState() { - super.initState(); - _loadActivities(); - _setModeImageURL(); - } - - Future _loadActivities() async { - setState(() { - _isLoading = true; - _error = null; - }); - - try { - final resp = await ActivityPlanGenerationRepo.get( - widget.controller.planRequest, - ); - _activities = resp.activityPlans; - } catch (e, s) { - _error = e; - ErrorHandler.logError( - e: e, - s: s, - data: { - 'activityPlanRequest': widget.controller.planRequest, - }, - ); - } finally { - if (mounted) setState(() => _isLoading = false); - } - } - - Future _setModeImageURL() async { - final mode = await _selectedMode; - if (mode == null) return; - - final modeName = - mode.defaultName.toLowerCase().replaceAll(RegExp(r'\s+'), ''); - final filename = - "${ActivitySuggestionsConstants.modeImageFileStart}$modeName.jpg"; - - if (!mounted) return; - setState(() { - _avatarURL = "${AppConfig.assetsBaseURL}/$filename"; - _filename = filename; - }); - } - - Future _onEdit(int index, ActivityPlanModel updatedActivity) async { - // in this case we're editing an activity plan that was generated recently - // via the repo and should be updated in the cached response - if (_activities != null) { - final activities = _activities; - activities?[index] = updatedActivity; - ActivityPlanGenerationRepo.set( - widget.controller.planRequest, - ActivityPlanResponse(activityPlans: _activities!), - ); - } - - setState(() {}); - } - - Future get _selectedMode async { - final modes = await widget.controller.modeItems; - return modes.firstWhereOrNull( - (element) => - element.name.toLowerCase() == - widget.controller.planRequest.mode.toLowerCase(), - ); - } - - @override - Widget build(BuildContext context) { - final l10n = L10n.of(context); - if (_isLoading) { - return const Padding( - padding: EdgeInsets.all(32.0), - child: Center(child: CircularProgressIndicator()), - ); - } else if (_error != null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(l10n.oopsSomethingWentWrong), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadActivities, - child: Text(l10n.tryAgain), - ), - ], - ), - ); - } else if (_activities == null || _activities!.isEmpty) { - return Center(child: Text(l10n.noDataFound)); - } else { - return Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _activities!.length, - itemBuilder: (context, index) { - return ActivityPlanCard( - activity: _activities![index], - room: widget.controller.room, - onEdit: (updatedActivity) => _onEdit(index, updatedActivity), - avatarURL: _avatarURL, - initialFilename: _filename, - onChange: () => setState(() {}), - ); - }, - ), - ); - } - } -} diff --git a/lib/pangea/activity_planner/new_activity_form.dart b/lib/pangea/activity_planner/new_activity_form.dart deleted file mode 100644 index c05702cfb..000000000 --- a/lib/pangea/activity_planner/new_activity_form.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart'; -import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; -import 'package:fluffychat/pangea/activity_planner/suggestion_form_field.dart'; -import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart'; -import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart'; -import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; -import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; -import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class NewActivityForm extends StatelessWidget { - final ActivityPlannerPageState controller; - const NewActivityForm(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - final l10n = L10n.of(context); - return Expanded( - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: Form( - key: controller.formKey, - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - const InstructionsInlineTooltip( - instructionsEnum: InstructionsEnum.activityPlannerOverview, - ), - Row( - children: [ - Expanded( - child: Column( - children: [ - SuggestionFormField( - suggestions: controller.topicItems, - validator: controller.validateNotNull, - label: l10n.topicLabel, - placeholder: l10n.topicPlaceholder, - controller: controller.topicController, - ), - const SizedBox(height: 24), - SuggestionFormField( - suggestions: controller.objectiveItems, - validator: controller.validateNotNull, - label: l10n.learningObjectiveLabel, - placeholder: l10n.learningObjectivePlaceholder, - controller: controller.objectiveController, - ), - const SizedBox(height: 24), - SuggestionFormField( - suggestions: controller.modeItems, - validator: controller.validateNotNull, - label: l10n.modeLabel, - placeholder: l10n.modePlaceholder, - controller: controller.modeController, - ), - ], - ), - ), - const SizedBox(width: 4), - Column( - children: [ - IconButton( - icon: const Icon(Icons.shuffle), - onPressed: controller.randomizeSelections, - ), - ], - ), - ], - ), - const SizedBox(height: 24), - DropdownButtonFormField2( - customButton: CustomDropdownTextButton( - text: controller.selectedMedia - .toDisplayCopyUsingL10n(context), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.zero, // Remove default padding - ), - decoration: InputDecoration(labelText: l10n.mediaLabel), - isExpanded: true, - dropdownStyleData: DropdownStyleData( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - ), - items: MediaEnum.values - .map( - (e) => DropdownMenuItem( - value: e, - child: DropdownTextButton( - text: e.toDisplayCopyUsingL10n(context), - isSelected: controller.selectedMedia == e, - ), - ), - ) - .toList(), - onChanged: controller.setSelectedMedia, - value: controller.selectedMedia, - ), - const SizedBox(height: 24), - LanguageLevelDropdown( - initialLevel: controller.selectedCefrLevel, - onChanged: controller.setSelectedCefrLevel, - ), - const SizedBox(height: 24), - PLanguageDropdown( - languages: - MatrixState.pangeaController.pLanguageStore.baseOptions, - onChange: (val) => controller - .setSelectedLanguageOfInstructions(val.langCode), - initialLanguage: controller.selectedLanguageOfInstructions != - null - ? PLanguageStore.byLangCode( - controller.selectedLanguageOfInstructions!, - ) - : MatrixState.pangeaController.languageController.userL1, - isL2List: false, - decorationText: L10n.of(context).languageOfInstructionsLabel, - ), - const SizedBox(height: 24), - PLanguageDropdown( - languages: - MatrixState.pangeaController.pLanguageStore.targetOptions, - onChange: (val) => - controller.setSelectedTargetLanguage(val.langCode), - initialLanguage: controller.selectedTargetLanguage != null - ? PLanguageStore.byLangCode( - controller.selectedTargetLanguage!, - ) - : MatrixState.pangeaController.languageController.userL2, - decorationText: L10n.of(context).targetLanguageLabel, - isL2List: true, - ), - const SizedBox(height: 24), - TextFormField( - decoration: InputDecoration( - labelText: l10n.numberOfLearners, - ), - textInputAction: TextInputAction.done, - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.mustBeInteger; - } - final n = int.tryParse(value); - if (n == null || n <= 0) { - return l10n.mustBeInteger; - } - return null; - }, - onChanged: (val) => controller - .setSelectedNumberOfParticipants(int.tryParse(val)), - initialValue: - controller.selectedNumberOfParticipants?.toString(), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), - onFieldSubmitted: (_) { - if (controller.formKey.currentState?.validate() ?? false) { - controller.generateActivities(); - } - }, - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - if (controller.formKey.currentState?.validate() ?? false) { - controller.generateActivities(); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.lightbulb_outline), - const SizedBox(width: 8), - Text(l10n.generateActivitiesButton), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index cff67c301..759bbea93 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -11,13 +11,19 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; import 'package:fluffychat/pangea/activity_suggestions/create_chat_card.dart'; +import 'package:fluffychat/pangea/activity_suggestions/make_activity_card.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ActivitySuggestionsArea extends StatefulWidget { final Axis? scrollDirection; - const ActivitySuggestionsArea({super.key, this.scrollDirection}); + final bool includeCustomCards; + const ActivitySuggestionsArea({ + super.key, + this.scrollDirection, + this.includeCustomCards = true, + }); @override ActivitySuggestionsAreaState createState() => ActivitySuggestionsAreaState(); } @@ -89,14 +95,25 @@ class ActivitySuggestionsAreaState extends State { .cast() .toList(); - cards.insert( - 0, - CreateChatCard( - width: cardWidth, - height: cardHeight, - padding: cardPadding, - ), - ); + if (widget.includeCustomCards) { + cards.insert( + 0, + CreateChatCard( + width: cardWidth, + height: cardHeight, + padding: cardPadding, + ), + ); + + cards.insert( + 1, + MakeActivityCard( + width: cardWidth, + height: cardHeight, + padding: cardPadding, + ), + ); + } final scrollDirection = widget.scrollDirection ?? (_isColumnMode ? Axis.horizontal : Axis.vertical); diff --git a/lib/pangea/activity_suggestions/activity_suggestions_constants.dart b/lib/pangea/activity_suggestions/activity_suggestions_constants.dart index ecb4c8c90..ac1c16209 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_constants.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_constants.dart @@ -1,4 +1,6 @@ class ActivitySuggestionsConstants { static const String plusIconPath = "add_icon.svg"; + static const String crayonIconPath = "make_your_own_icon.svg"; static const String modeImageFileStart = "activityplanner_mode_"; + static const String makeActivityAssetPath = "Spark+imaginative.png"; } diff --git a/lib/pangea/activity_suggestions/make_activity_card.dart b/lib/pangea/activity_suggestions/make_activity_card.dart new file mode 100644 index 000000000..17eea5ed3 --- /dev/null +++ b/lib/pangea/activity_suggestions/make_activity_card.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; +import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; +import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; + +class MakeActivityCard extends StatelessWidget { + final double width; + final double height; + final double padding; + + const MakeActivityCard({ + required this.width, + required this.height, + required this.padding, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: EdgeInsets.all(padding), + child: PressableButton( + onPressed: () => context.go('/rooms/planner'), + borderRadius: BorderRadius.circular(24.0), + color: theme.colorScheme.primary, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(24.0), + ), + height: height, + width: width, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: CustomizedSvg( + svgUrl: + "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}", + colorReplacements: { + "#CDBEF9": + colorToHex(Theme.of(context).colorScheme.secondary), + }, + height: 80, + width: 80, + ), + ), + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context).makeYourOwn, + style: theme.textTheme.bodyLarge + ?.copyWith(color: theme.colorScheme.secondary), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ); + } +}