2223 make your own activity page (#2245)

* feat: added make your own activity page

* chore: center content of activity planner page
This commit is contained in:
ggurdin 2025-03-27 16:57:07 -04:00 committed by GitHub
parent 66ac13f3bf
commit b7b5522649
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 731 additions and 565 deletions

View file

@ -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"
}

View file

@ -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',

View file

@ -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<ActivityGenerator> {
bool loading = false;
String? error;
List<ActivityPlanModel>? activities;
final formKey = GlobalKey<FormState>();
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<List<ActivitySettingResponseSchema>> get topicItems =>
TopicListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get modeItems =>
ActivityModeListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get objectiveItems =>
LearningObjectiveListRepo.get(req);
String? validateNotNull(String? value) {
if (value == null || value.isEmpty) {
return L10n.of(context).interactiveTranslatorRequired;
}
return null;
}
Future<String> _randomTopic() async {
final topics = await topicItems;
return (topics..shuffle()).first.name;
}
Future<String> _randomObjective() async {
final objectives = await objectiveItems;
return (objectives..shuffle()).first.name;
}
Future<String> _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<ActivitySettingResponseSchema?> get _selectedMode async {
final modes = await modeItems;
return modes.firstWhereOrNull(
(element) => element.name.toLowerCase() == planRequest.mode.toLowerCase(),
);
}
Future<void> _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<void> 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<void> 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);
}

View file

@ -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),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -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<ActivityPlanCard> {
});
}
Future<void> _onLaunch() => showFutureLoadingDialog(
context: context,
future: () async {
Future<void> _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<ActivityPlanCard> {
);
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);

View file

@ -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<ActivityPlannerPage> {
final formKey = GlobalKey<FormState>();
PageMode pageMode = PageMode.featuredActivities;
MediaEnum selectedMedia = MediaEnum.nan;
String? selectedLanguageOfInstructions;
String? selectedTargetLanguage;
LanguageLevelTypeEnum? selectedCefrLevel;
int? selectedNumberOfParticipants;
List<String> 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<List<ActivitySettingResponseSchema>> get topicItems =>
TopicListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get modeItems =>
ActivityModeListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> 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<void> generateActivities() async =>
_setPageMode(PageMode.generatedActivities);
Future<String> _randomTopic() async {
final topics = await topicItems;
return (topics..shuffle()).first.name;
}
Future<String> _randomObjective() async {
final objectives = await objectiveItems;
return (objectives..shuffle()).first.name;
}
Future<String> _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<ActivityPlannerPage> {
child: SingleChildScrollView(
child: ActivitySuggestionsArea(
scrollDirection: Axis.vertical,
includeCustomCards: false,
),
),
);
@ -218,36 +59,61 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
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<PageMode>(
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<PageMode>(
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,
],
),
],
),
),
),
);

View file

@ -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),
],
);
}

View file

@ -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<GeneratedActivitiesList> {
List<ActivityPlanModel>? _activities;
bool _isLoading = true;
Object? _error;
String? _avatarURL;
String? _filename;
@override
void initState() {
super.initState();
_loadActivities();
_setModeImageURL();
}
Future<void> _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<void> _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<void> _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<ActivitySettingResponseSchema?> 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(() {}),
);
},
),
);
}
}
}

View file

@ -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<MediaEnum>(
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),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -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<ActivitySuggestionsArea> {
.cast<Widget>()
.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);

View file

@ -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";
}

View file

@ -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,
),
),
],
),
),
),
);
}
}