diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ae9352c89..777846143 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4768,5 +4768,7 @@ "voiceMessage": "Voice message", "nan": "Not applicable", "activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!", - "completeActivitiesToUnlock": "Complete the highlighted word activities to unlock" + "completeActivitiesToUnlock": "Complete the highlighted word activities to unlock", + "myBookmarkedActivities": "My Bookmarked Activities", + "noBookmarkedActivities": "No bookmarked activities" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3e11711be..dc819eaf8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,4 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/learning_settings/utils/language_list_util.dar import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/error_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pangea/activity_planner/activity_list_view.dart b/lib/pangea/activity_planner/activity_list_view.dart new file mode 100644 index 000000000..e6659a8e7 --- /dev/null +++ b/lib/pangea/activity_planner/activity_list_view.dart @@ -0,0 +1,147 @@ +import 'dart:developer'; + +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/bookmarked_activities_repo.dart'; +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'activity_plan_card.dart'; + +class ActivityListView extends StatefulWidget { + final Room? room; + + /// if null, show saved activities + final ActivityPlanRequest? activityPlanRequest; + + const ActivityListView({ + super.key, + required this.room, + required this.activityPlanRequest, + }); + + @override + _ActivityListViewState createState() => _ActivityListViewState(); +} + +class _ActivityListViewState extends State { + bool get showBookmarkedActivities => widget.activityPlanRequest == null; + + Future> get savedActivities => + Future.value(BookmarkedActivitiesRepo.get()); + + Future> get activities async => + showBookmarkedActivities + ? await savedActivities + : (await ActivityPlanGenerationRepo.get(widget.activityPlanRequest!)) + .activityPlans; + + @override + void dispose() { + super.dispose(); + } + + 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 (widget.activityPlanRequest != null) { + final activities = await this.activities; + activities[index] = updatedActivity; + ActivityPlanGenerationRepo.set( + widget.activityPlanRequest!, + ActivityPlanResponse(activityPlans: activities), + ); + } + + setState(() {}); + } + + Future _onLaunch(int index) => showFutureLoadingDialog( + context: context, + future: () async { + final activity = (await activities)[index]; + + final eventId = await widget.room?.pangeaSendTextEvent( + activity.markdown, + messageTag: ModelKey.messageTagActivityPlan, + //include full model or should we move to a state event for this? + ); + + if (eventId == null) { + debugger(when: kDebugMode); + return; + } + + await widget.room?.setPinnedEvents([eventId]); + + Navigator.of(context).pop(); + }, + ); + + @override + Widget build(BuildContext context) { + final l10n = L10n.of(context); + return FutureBuilder>( + future: activities, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + print(snapshot.error); + print(snapshot.stackTrace); + // debugger(when: kDebugMode); + ErrorHandler.logError( + e: snapshot.error, + s: snapshot.stackTrace, + data: { + 'room': widget.room, + 'activityPlanRequest': widget.activityPlanRequest, + 'snapshot.data': snapshot.data, + 'snapshot.error': snapshot.error, + }, + ); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(l10n.oopsSomethingWentWrong), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() {}); + }, + child: Text(l10n.tryAgain), + ), + ], + ), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + if (showBookmarkedActivities) { + return Center(child: Text(l10n.noBookmarkedActivities)); + } + return Center(child: Text(l10n.noDataFound)); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + return ActivityPlanCard( + activity: snapshot.data![index], + onLaunch: () => _onLaunch(index), + onEdit: (updatedActivity) => _onEdit(index, updatedActivity), + ); + }, + ); + } + }, + ); + } +} diff --git a/lib/pangea/activity_planner/activity_mode_list_repo.dart b/lib/pangea/activity_planner/activity_mode_list_repo.dart index 9b4db5172..66d6220d8 100644 --- a/lib/pangea/activity_planner/activity_mode_list_repo.dart +++ b/lib/pangea/activity_planner/activity_mode_list_repo.dart @@ -1,12 +1,12 @@ import 'dart:convert'; -import 'package:get_storage/get_storage.dart'; -import 'package:http/http.dart'; - import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + import '../common/network/requests.dart'; class ActivityModeListRepo { diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart new file mode 100644 index 000000000..dbf7e83a6 --- /dev/null +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -0,0 +1,305 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class ActivityPlanCard extends StatefulWidget { + final ActivityPlanModel activity; + final VoidCallback onLaunch; + final ValueChanged onEdit; + final double maxWidth; + + const ActivityPlanCard({ + super.key, + required this.activity, + required this.onLaunch, + required this.onEdit, + this.maxWidth = 400, + }); + + @override + ActivityPlanCardState createState() => ActivityPlanCardState(); +} + +class ActivityPlanCardState extends State { + bool _isEditing = false; + late ActivityPlanModel _tempActivity; + late TextEditingController _titleController; + late TextEditingController _learningObjectiveController; + late TextEditingController _instructionsController; + final TextEditingController _newVocabController = TextEditingController(); + + @override + void initState() { + super.initState(); + _tempActivity = widget.activity; + _titleController = TextEditingController(text: _tempActivity.title); + _learningObjectiveController = + TextEditingController(text: _tempActivity.learningObjective); + _instructionsController = + TextEditingController(text: _tempActivity.instructions); + } + + static const double itemPadding = 8; + + @override + void dispose() { + _titleController.dispose(); + _learningObjectiveController.dispose(); + _instructionsController.dispose(); + _newVocabController.dispose(); + super.dispose(); + } + + Future _saveEdits() async { + final updatedActivity = ActivityPlanModel( + req: _tempActivity.req, + title: _titleController.text, + learningObjective: _learningObjectiveController.text, + instructions: _instructionsController.text, + vocab: _tempActivity.vocab, + ); + + final activityWithBookmarkId = await _addBookmark(updatedActivity); + + // need to save in the repo as well + widget.onEdit(activityWithBookmarkId); + + setState(() { + _isEditing = false; + }); + } + + Future _addBookmark(ActivityPlanModel activity) => + BookmarkedActivitiesRepo.save(activity).catchError((e, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: stack, data: activity.toJson()); + }).whenComplete(() { + setState(() {}); + }); + + Future _removeBookmark() => + BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId!) + .catchError((e, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson()); + }).whenComplete(() { + setState(() {}); + }); + + void _addVocab() { + setState(() { + _tempActivity.vocab.add(Vocab(lemma: _newVocabController.text, pos: '')); + _newVocabController.clear(); + }); + } + + void _removeVocab(int index) { + setState(() { + _tempActivity.vocab.removeAt(index); + }); + } + + bool get isBookmarked => + BookmarkedActivitiesRepo.isBookmarked(widget.activity); + + @override + Widget build(BuildContext context) { + final l10n = L10n.of(context); + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.maxWidth), + child: Card( + margin: const EdgeInsets.symmetric(vertical: itemPadding), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Icons.event_note_outlined), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _titleController, + decoration: InputDecoration( + labelText: L10n.of(context).title, + ), + maxLines: null, + ) + : Text( + widget.activity.title, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + if (!_isEditing) + IconButton( + onPressed: isBookmarked + ? () => _removeBookmark() + : () => _addBookmark(widget.activity), + icon: Icon( + isBookmarked ? Icons.bookmark : Icons.bookmark_border, + ), + ), + ], + ), + const SizedBox(height: itemPadding), + Row( + children: [ + Icon( + Symbols.target, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _learningObjectiveController, + decoration: const InputDecoration( + labelText: 'Learning Objective', + ), + maxLines: null, + ) + : Text( + widget.activity.learningObjective, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: itemPadding), + Row( + children: [ + Icon( + Symbols.steps_rounded, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: _isEditing + ? TextField( + controller: _instructionsController, + decoration: const InputDecoration( + labelText: 'Instructions', + ), + maxLines: null, + ) + : Text( + widget.activity.instructions, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: itemPadding), + if (widget.activity.vocab.isNotEmpty) ...[ + Row( + children: [ + Icon( + Symbols.dictionary, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: itemPadding), + Expanded( + child: Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: List.generate( + _tempActivity.vocab.length, (int index) { + return _isEditing + ? Chip( + label: + Text(_tempActivity.vocab[index].lemma), + onDeleted: () => _removeVocab(index), + backgroundColor: Colors.transparent, + visualDensity: VisualDensity.compact, + shape: const StadiumBorder( + side: + BorderSide(color: Colors.transparent), + ), + ) + : Chip( + label: + Text(_tempActivity.vocab[index].lemma), + backgroundColor: Colors.transparent, + visualDensity: VisualDensity.compact, + shape: const StadiumBorder( + side: + BorderSide(color: Colors.transparent), + ), + ); + }).toList(), + ), + ), + ], + ), + if (_isEditing) + Padding( + padding: const EdgeInsets.only(top: itemPadding), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _newVocabController, + decoration: const InputDecoration( + labelText: 'Add Vocabulary', + ), + ), + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: _addVocab, + ), + ], + ), + ), + ], + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + icon: Icon(!_isEditing ? Icons.edit : Icons.save), + onPressed: () => !_isEditing + ? setState(() { + _isEditing = true; + }) + : _saveEdits(), + isSelected: _isEditing, + ), + if (_isEditing) + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + setState(() { + _isEditing = false; + }); + }, + ), + ], + ), + ElevatedButton.icon( + onPressed: !_isEditing ? widget.onLaunch : null, + icon: const Icon(Icons.send), + label: Text(l10n.launchActivityButton), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_planner/activity_plan_generation_repo.dart b/lib/pangea/activity_planner/activity_plan_generation_repo.dart index 574933c9e..c693c92ac 100644 --- a/lib/pangea/activity_planner/activity_plan_generation_repo.dart +++ b/lib/pangea/activity_planner/activity_plan_generation_repo.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart'; @@ -9,83 +11,6 @@ import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../common/network/requests.dart'; -class ActivityPlanRequest { - final String topic; - final String mode; - final String objective; - final MediaEnum media; - final int cefrLevel; - final String languageOfInstructions; - final String targetLanguage; - final int count; - - ActivityPlanRequest({ - required this.topic, - required this.mode, - required this.objective, - required this.media, - required this.cefrLevel, - required this.languageOfInstructions, - required this.targetLanguage, - this.count = 3, - }); - - Map toJson() { - return { - 'topic': topic, - 'mode': mode, - 'objective': objective, - 'media': media.string, - 'cefr_level': cefrLanguageLevel, - 'language_of_instructions': languageOfInstructions, - 'target_language': targetLanguage, - 'count': count, - }; - } - - String get storageKey => - '$topic-$mode-$objective-${media.string}-$cefrLevel-$languageOfInstructions-$targetLanguage'; - - String get cefrLanguageLevel { - switch (cefrLevel) { - case 0: - return 'Pre-A1'; - case 1: - return 'A1'; - case 2: - return 'A2'; - case 3: - return 'B1'; - case 4: - return 'B2'; - case 5: - return 'C1'; - case 6: - return 'C2'; - default: - return 'Pre-A1'; - } - } -} - -class ActivityPlanResponse { - final List activityPlans; - - ActivityPlanResponse({required this.activityPlans}); - - factory ActivityPlanResponse.fromJson(Map json) { - return ActivityPlanResponse( - activityPlans: List.from(json['activity_plans']), - ); - } - - Map toJson() { - return { - 'activity_plans': activityPlans, - }; - } -} - class ActivityPlanGenerationRepo { static final GetStorage _activityPlanStorage = GetStorage('activity_plan_storage'); diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart new file mode 100644 index 000000000..da2e44287 --- /dev/null +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -0,0 +1,90 @@ +import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; + +class ActivityPlanModel { + final ActivityPlanRequest req; + final String title; + final String learningObjective; + final String instructions; + final List vocab; + String? bookmarkId; + + ActivityPlanModel({ + required this.req, + required this.title, + required this.learningObjective, + required this.instructions, + required this.vocab, + this.bookmarkId, + }); + + factory ActivityPlanModel.fromJson(Map json) { + return ActivityPlanModel( + req: ActivityPlanRequest.fromJson(json['req']), + title: json['title'], + learningObjective: json['learning_objective'], + instructions: json['instructions'], + vocab: List.from( + json['vocab'].map((vocab) => Vocab.fromJson(vocab)), + ), + bookmarkId: json['bookmark_id'], + ); + } + + Map toJson() { + return { + 'req': req.toJson(), + 'title': title, + 'learning_objective': learningObjective, + 'instructions': instructions, + 'vocab': vocab.map((vocab) => vocab.toJson()).toList(), + 'bookmark_id': bookmarkId, + }; + } + + /// activity content displayed nicely in markdown + /// use target emoji for learning objective + /// use step emoji for instructions + String get markdown { + String markdown = + ''' **$title** \n🎯 $learningObjective \n🪜 $instructions \n📖 '''; + // cycle through vocab with index + for (var i = 0; i < vocab.length; i++) { + // if the lemma appears more than once in the vocab list, show the pos + // vocab is a wrapped list of string, separated by commas + final v = vocab[i]; + final bool showPos = + vocab.where((vocab) => vocab.lemma == v.lemma).length > 1; + markdown += + '${v.lemma}${showPos ? ' (${v.pos})' : ''}${i + 1 < vocab.length ? ', ' : ''}'; + } + return markdown; + } + + bool get isBookmarked { + return bookmarkId != null; + } +} + +class Vocab { + final String lemma; + final String pos; + + Vocab({ + required this.lemma, + required this.pos, + }); + + factory Vocab.fromJson(Map json) { + return Vocab( + lemma: json['lemma'], + pos: json['pos'], + ); + } + + Map toJson() { + return { + 'lemma': lemma, + 'pos': pos, + }; + } +} diff --git a/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart b/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart index c69fbaa46..e48925836 100644 --- a/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart +++ b/lib/pangea/activity_planner/activity_plan_page_launch_icon_button.dart @@ -1,10 +1,9 @@ +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:fluffychat/pages/chat/chat.dart'; - class ActivityPlanPageLaunchIconButton extends StatelessWidget { const ActivityPlanPageLaunchIconButton({ super.key, @@ -15,6 +14,9 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget { @override Widget build(BuildContext context) { + if (controller.room.isBotDM) { + return const SizedBox(); + } return IconButton( icon: const Icon(Icons.event_note_outlined), tooltip: L10n.of(context).activityPlannerTitle, diff --git a/lib/pangea/activity_planner/activity_plan_request.dart b/lib/pangea/activity_planner/activity_plan_request.dart new file mode 100644 index 000000000..a648f73e9 --- /dev/null +++ b/lib/pangea/activity_planner/activity_plan_request.dart @@ -0,0 +1,97 @@ +import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; + +class ActivityPlanRequest { + final String topic; + final String mode; + final String objective; + final MediaEnum media; + final int cefrLevel; + final String languageOfInstructions; + final String targetLanguage; + final int count; + + ActivityPlanRequest({ + required this.topic, + required this.mode, + required this.objective, + required this.media, + required this.cefrLevel, + required this.languageOfInstructions, + required this.targetLanguage, + this.count = 3, + }); + + Map toJson() { + return { + 'topic': topic, + 'mode': mode, + 'objective': objective, + 'media': media.string, + 'cefr_level': cefrLanguageLevel, + 'language_of_instructions': languageOfInstructions, + 'target_language': targetLanguage, + 'count': count, + }; + } + + factory ActivityPlanRequest.fromJson(Map json) { + int cefrLevel = 0; + switch (json['cefr_level']) { + case 'Pre-A1': + cefrLevel = 0; + break; + case 'A1': + cefrLevel = 1; + break; + case 'A2': + cefrLevel = 2; + break; + case 'B1': + cefrLevel = 3; + break; + case 'B2': + cefrLevel = 4; + break; + case 'C1': + cefrLevel = 5; + break; + case 'C2': + cefrLevel = 6; + break; + } + return ActivityPlanRequest( + topic: json['topic'], + mode: json['mode'], + objective: json['objective'], + media: MediaEnum.nan.fromString(json['media']), + cefrLevel: cefrLevel, + languageOfInstructions: json['language_of_instructions'], + targetLanguage: json['target_language'], + count: json['count'], + ); + } + + String get storageKey => + '$topic-$mode-$objective-${media.string}-$cefrLevel-$languageOfInstructions-$targetLanguage'; + + String get cefrLanguageLevel { + switch (cefrLevel) { + case 0: + return 'Pre-A1'; + case 1: + return 'A1'; + case 2: + return 'A2'; + case 3: + return 'B1'; + case 4: + return 'B2'; + case 5: + return 'C1'; + case 6: + return 'C2'; + default: + return 'Pre-A1'; + } + } +} diff --git a/lib/pangea/activity_planner/activity_plan_response.dart b/lib/pangea/activity_planner/activity_plan_response.dart new file mode 100644 index 000000000..d0c216ef0 --- /dev/null +++ b/lib/pangea/activity_planner/activity_plan_response.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; + +class ActivityPlanResponse { + final List activityPlans; + + ActivityPlanResponse({required this.activityPlans}); + + factory ActivityPlanResponse.fromJson(Map json) { + return ActivityPlanResponse( + activityPlans: (json['activity_plans'] as List) + .map((e) => ActivityPlanModel.fromJson(e)) + .toList(), + ); + } + + Map toJson() { + return { + 'activity_plans': activityPlans.map((e) => e.toJson()).toList(), + }; + } +} diff --git a/lib/pangea/activity_planner/activity_plan_tile.dart b/lib/pangea/activity_planner/activity_plan_tile.dart deleted file mode 100644 index 35d4d78cf..000000000 --- a/lib/pangea/activity_planner/activity_plan_tile.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ActivityPlanTile extends StatefulWidget { - final String activity; - final VoidCallback onLaunch; - final ValueChanged onEdit; - - const ActivityPlanTile({ - super.key, - required this.activity, - required this.onLaunch, - required this.onEdit, - }); - - @override - ActivityPlanTileState createState() => ActivityPlanTileState(); -} - -class ActivityPlanTileState extends State { - late TextEditingController _controller; - bool editMode = false; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.activity); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = L10n.of(context); - return Card( - margin: const EdgeInsets.all(8.0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (editMode) - TextField( - controller: _controller, - onChanged: widget.onEdit, - maxLines: null, - ) - else - Text( - widget.activity, - maxLines: null, - overflow: TextOverflow.visible, - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 8.0), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () { - setState(() { - editMode = !editMode; - }); - }, - child: Text(!editMode ? l10n.edit : l10n.cancel), - ), - const SizedBox(width: 8.0), - ElevatedButton( - onPressed: !editMode - ? widget.onLaunch - : () { - setState(() { - widget.onEdit(_controller.text); - editMode = !editMode; - }); - }, - child: Text( - !editMode ? l10n.launchActivityButton : l10n.saveChanges, - ), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/activity_planner/activity_planner_page.dart b/lib/pangea/activity_planner/activity_planner_page.dart index a71a585f0..a9ef2e3b7 100644 --- a/lib/pangea/activity_planner/activity_planner_page.dart +++ b/lib/pangea/activity_planner/activity_planner_page.dart @@ -1,60 +1,26 @@ -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - +import 'package:fluffychat/pangea/activity_planner/activity_list_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_tile.dart'; +import 'package:fluffychat/pangea/activity_planner/activity_plan_request.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/suggestion_form_field.dart'; import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; -import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/events/repo/token_api_models.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; - -// a page to allow the user to choose settings and then generate a list of activities -// has app bar with a back button to go back to content 1 (disabled if on content 1), and a title of "Activity Planner", and close button to close the activity planner -// content 1 - settings -// content 2 - display of activities generated by the system, allowing edit and selection - -// use standard flutter material widgets and theme colors/styles. all copy should be defined in intl_en.arb and used with L10n.of(context).copyKey - -// content 1 -// should have a maxWidth, pulled from appconfig -// a. topic input with drop-down of suggestions pulled from TopicListRepo. label text of "topic" and placeholder of some random suggestions from repo -// b. mode input with drop-down of suggestions pulled from ModeListRepo. label text of "mode" and placeholder of some random suggestions from repo -// c. objective input with drop-down of suggestions pulled from LearningObjectiveListRepo. label text of "learning objective" and placeholder of some random suggestions from repo. -// e. dropdown for media type with text "media students should share as part of the activity" -// d. dropdown for selecting "language of activity instructions" which is auto-populated with the user's l1 but can be changed with options coming from pangeaController.pLanguageStore.baseOptions -// f. dropdown for selecting "target language" which is auto-populated with the user's l2 but can be changed with options coming from pangeaController.pLanguageStore.targetOptions -// g. selection for language level -// h. button to generate activities - -// content 2 -// a. app bar with a back button to go back to content 1, and a title of "Activity Planner", and close button to close the activity planner -// b. display of activities generated by the system, arranged in a column. if there is enough horizontal space, the activities should be arranged in a row -// a1. each activity should have a button to "launch activity" which calls a callback. this can be blank for now. -// a2. each activity should have a button to edit the activity. upon edit, the activity should become an input form where the user can freely edit the activity content +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; enum _PageMode { settings, - activities, + generatedActivities, + savedActivities, } class ActivityPlannerPage extends StatefulWidget { @@ -80,18 +46,8 @@ class ActivityPlannerPageState extends State { String? _selectedTargetLanguage; int? _selectedCefrLevel; - /// fetch data from repos - List _topicItems = []; - List _modeItems = []; - List _objectiveItems = []; + List activities = []; - /// List of activities generated by the system - List _activities = []; - - final _topicSearchController = TextEditingController(); - final _objectiveSearchController = TextEditingController(); - - final List _activityControllers = []; Room? get room => Matrix.of(context).client.getRoomById(widget.roomID); @override @@ -102,138 +58,69 @@ class ActivityPlannerPageState extends State { return; } - _loadDropdownData(); - _selectedLanguageOfInstructions = MatrixState.pangeaController.languageController.userL1?.langCode; _selectedTargetLanguage = MatrixState.pangeaController.languageController.userL2?.langCode; _selectedCefrLevel = 0; - - // Initialize controllers for activity editing - for (final activity in _activities) { - _activityControllers.add(TextEditingController(text: activity)); - } } + final _topicController = TextEditingController(); + final _objectiveController = TextEditingController(); + final _modeController = TextEditingController(); + @override void dispose() { - _topicSearchController.dispose(); - _objectiveSearchController.dispose(); - disposeAndClearActivityControllers(); + _topicController.dispose(); + _objectiveController.dispose(); + _modeController.dispose(); super.dispose(); } - void disposeAndClearActivityControllers() { - for (final controller in _activityControllers) { - controller.dispose(); - } - _activityControllers.clear(); - } - ActivitySettingRequestSchema get req => ActivitySettingRequestSchema( langCode: MatrixState.pangeaController.languageController.userL2?.langCode ?? LanguageKeys.defaultLanguage, ); - Future _loadDropdownData() async { - final topics = await TopicListRepo.get(req); - final modes = await ActivityModeListRepo.get(req); - final objectives = await LearningObjectiveListRepo.get(req); - setState(() { - _topicItems = topics; - _modeItems = modes; - _objectiveItems = objectives; - }); - } + Future> get _topicItems => + TopicListRepo.get(req); - // send the activity as a message to the room - Future onLaunch(int index) => showFutureLoadingDialog( - context: context, - future: () async { - // this shouldn't often error but just in case since it's not necessary for the activity to be sent - List? tokens; - try { - tokens = await MatrixState.pangeaController.messageData.getTokens( - repEventId: null, - req: TokensRequestModel( - fullText: _activities[index], - langCode: _selectedLanguageOfInstructions!, - senderL1: _selectedLanguageOfInstructions!, - senderL2: _selectedLanguageOfInstructions!, - ), - room: null, - ); - } catch (e) { - debugger(when: kDebugMode); - } + Future> get _modeItems => + ActivityModeListRepo.get(req); - final eventId = await room?.pangeaSendTextEvent( - _activities[index], - messageTag: ModelKey.messageTagActivityPlan, - originalSent: PangeaRepresentation( - langCode: _selectedLanguageOfInstructions!, - text: _activities[index], - originalSent: true, - originalWritten: false, - ), - tokensSent: - tokens != null ? PangeaMessageTokens(tokens: tokens) : null, - ); - - if (eventId == null) { - debugger(when: kDebugMode); - return; - } - - await room?.setPinnedEvents([eventId]); - - Navigator.of(context).pop(); - }, - ); + Future> get _objectiveItems => + LearningObjectiveListRepo.get(req); Future _generateActivities() async { - if (_formKey.currentState?.validate() ?? false) { - final request = ActivityPlanRequest( - topic: _selectedTopic!, - mode: _selectedMode!, - objective: _selectedObjective!, - media: _selectedMedia, - languageOfInstructions: _selectedLanguageOfInstructions!, - targetLanguage: _selectedTargetLanguage!, - cefrLevel: _selectedCefrLevel!, - ); - - await showFutureLoadingDialog( - context: context, - future: () async { - final response = await ActivityPlanGenerationRepo.get(request); - setState(() { - _activities = response.activityPlans; - disposeAndClearActivityControllers(); - for (final activity in _activities) { - _activityControllers.add(TextEditingController(text: activity)); - } - _pageMode = _PageMode.activities; - }); - }, - ); - } + _pageMode = _PageMode.generatedActivities; + setState(() {}); } - bool get _canRandomizeSelections => - _topicItems.isNotEmpty && - _objectiveItems.isNotEmpty && - _modeItems.isNotEmpty; + Future _randomTopic() async { + final topics = await _topicItems; + return (topics..shuffle()).first.name; + } - void _randomizeSelections() { - if (!_canRandomizeSelections) return; + 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 { + _selectedTopic = await _randomTopic(); + _selectedObjective = await _randomObjective(); + _selectedMode = await _randomMode(); setState(() { - _selectedTopic = (_topicItems..shuffle()).first.name; - _selectedObjective = (_objectiveItems..shuffle()).first.name; - _selectedMode = (_modeItems..shuffle()).first.name; + _topicController.text = _selectedTopic!; + _objectiveController.text = _selectedObjective!; + _modeController.text = _selectedMode!; }); } @@ -259,223 +146,157 @@ class ActivityPlannerPageState extends State { onPressed: () => setState(() => _pageMode = _PageMode.settings), icon: const Icon(Icons.arrow_back), ), - title: Text(l10n.activityPlannerTitle), + title: _pageMode == _PageMode.savedActivities + ? Text(l10n.myBookmarkedActivities) + : Text(l10n.activityPlannerTitle), + actions: [ + IconButton( + onPressed: () => + setState(() => _pageMode = _PageMode.savedActivities), + icon: const Icon(Icons.bookmarks), + ), + ], ), - body: _pageMode == _PageMode.settings - ? _buildSettingsForm(l10n) - : _buildActivitiesView(l10n), - ); - } - - Widget _buildSettingsForm(L10n l10n) { - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: Form( - key: _formKey, - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - const InstructionsInlineTooltip( - instructionsEnum: InstructionsEnum.activityPlannerOverview, - ), - DropdownButtonFormField2( - hint: Text(l10n.topicPlaceholder), - value: _selectedTopic, - decoration: _selectedTopic != null - ? InputDecoration( - labelText: l10n.topicLabel, - ) - : null, - isExpanded: true, - validator: (value) => _validateNotNull(value), - dropdownSearchData: DropdownSearchData( - searchController: _topicSearchController, - searchInnerWidget: InnerSearchWidget( - searchController: _topicSearchController, - ), - searchInnerWidgetHeight: 60, - searchMatchFn: (item, searchValue) { - return item.value - .toString() - .toLowerCase() - .contains(searchValue.toLowerCase()); - }, - ), - items: _topicItems - .map( - (e) => DropdownMenuItem( - value: e.name, - child: Text(e.name), - ), - ) - .toList(), - onChanged: (value) { - _selectedTopic = value; - }, - dropdownStyleData: const DropdownStyleData(maxHeight: 400), - ), - const SizedBox(height: 24), - DropdownButtonFormField2( - hint: Text(l10n.learningObjectivePlaceholder), - decoration: _selectedObjective != null - ? InputDecoration(labelText: l10n.learningObjectiveLabel) - : null, - validator: (value) => _validateNotNull(value), - items: _objectiveItems - .map( - (e) => - DropdownMenuItem(value: e.name, child: Text(e.name)), - ) - .toList(), - onChanged: (val) => _selectedObjective = val, - dropdownStyleData: const DropdownStyleData(maxHeight: 400), - value: _selectedObjective, - dropdownSearchData: DropdownSearchData( - searchController: _objectiveSearchController, - searchInnerWidget: InnerSearchWidget( - searchController: _objectiveSearchController, - ), - searchInnerWidgetHeight: 60, - searchMatchFn: (item, searchValue) { - return item.value - .toString() - .toLowerCase() - .contains(searchValue.toLowerCase()); - }, - ), - ), - const SizedBox(height: 24), - DropdownButtonFormField2( - decoration: _selectedMode != null - ? InputDecoration(labelText: l10n.modeLabel) - : null, - hint: Text(l10n.modePlaceholder), - validator: (value) => _validateNotNull(value), - items: _modeItems - .map( - (e) => - DropdownMenuItem(value: e.name, child: Text(e.name)), - ) - .toList(), - onChanged: (val) => _selectedMode = val, - value: _selectedMode, - ), - const SizedBox(height: 24), - DropdownButtonFormField2( - decoration: InputDecoration(labelText: l10n.mediaLabel), - items: MediaEnum.values - .map( - (e) => DropdownMenuItem( - value: e, - child: Text(e.toDisplayCopyUsingL10n(context)), - ), - ) - .toList(), - onChanged: (val) => _selectedMedia = val ?? MediaEnum.nan, - value: _selectedMedia, - ), - const SizedBox(height: 24), - LanguageLevelDropdown( - initialLevel: 0, - onChanged: (val) => _selectedCefrLevel = val, - ), - const SizedBox(height: 24), - PLanguageDropdown( - languages: - MatrixState.pangeaController.pLanguageStore.baseOptions, - onChange: (val) => _selectedTargetLanguage = val.langCode, - initialLanguage: - MatrixState.pangeaController.languageController.userL1, - isL2List: false, - decorationText: L10n.of(context).languageOfInstructionsLabel, - ), - const SizedBox(height: 24), - PLanguageDropdown( - languages: - MatrixState.pangeaController.pLanguageStore.targetOptions, - onChange: (val) => _selectedTargetLanguage = val.langCode, - initialLanguage: - MatrixState.pangeaController.languageController.userL2, - decorationText: L10n.of(context).targetLanguageLabel, - isL2List: true, - ), - const SizedBox(height: 24), - Row( - children: [ - IconButton( - icon: const Icon(Icons.shuffle), - onPressed: - _canRandomizeSelections ? _randomizeSelections : null, - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: _generateActivities, - child: Text(l10n.generateActivitiesButton), + body: _pageMode != _PageMode.settings + ? ActivityListView( + room: room, + activityPlanRequest: _PageMode.savedActivities == _pageMode + ? null + : ActivityPlanRequest( + topic: _selectedTopic!, + mode: _selectedMode!, + objective: _selectedObjective!, + media: _selectedMedia, + languageOfInstructions: _selectedLanguageOfInstructions!, + targetLanguage: _selectedTargetLanguage!, + cefrLevel: _selectedCefrLevel!, ), + ) + : Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + const InstructionsInlineTooltip( + instructionsEnum: + InstructionsEnum.activityPlannerOverview, + ), + Row( + children: [ + Expanded( + child: Column( + children: [ + SuggestionFormField( + suggestions: _topicItems, + validator: _validateNotNull, + label: l10n.topicLabel, + placeholder: l10n.topicPlaceholder, + onSelected: (val) => _selectedTopic = val, + initialValue: _selectedTopic, + controller: _topicController, + ), + const SizedBox(height: 24), + SuggestionFormField( + suggestions: _objectiveItems, + validator: _validateNotNull, + label: l10n.learningObjectiveLabel, + placeholder: + l10n.learningObjectivePlaceholder, + onSelected: (val) => _selectedObjective = val, + initialValue: _selectedObjective, + controller: _objectiveController, + ), + const SizedBox(height: 24), + SuggestionFormField( + suggestions: _modeItems, + validator: _validateNotNull, + label: l10n.modeLabel, + placeholder: l10n.modePlaceholder, + onSelected: (val) => _selectedMode = val, + initialValue: _selectedMode, + controller: _modeController, + ), + ], + ), + ), + const SizedBox(width: 4), + Column( + children: [ + IconButton( + icon: const Icon(Icons.shuffle), + onPressed: _randomizeSelections, + ), + ], + ), + ], + ), + const SizedBox(height: 24), + DropdownButtonFormField2( + decoration: InputDecoration(labelText: l10n.mediaLabel), + items: MediaEnum.values + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.toDisplayCopyUsingL10n(context)), + ), + ) + .toList(), + onChanged: (val) => + _selectedMedia = val ?? MediaEnum.nan, + value: _selectedMedia, + ), + const SizedBox(height: 24), + LanguageLevelDropdown( + initialLevel: 0, + onChanged: (val) => _selectedCefrLevel = val, + ), + const SizedBox(height: 24), + PLanguageDropdown( + languages: MatrixState + .pangeaController.pLanguageStore.baseOptions, + onChange: (val) => + _selectedTargetLanguage = val.langCode, + initialLanguage: MatrixState + .pangeaController.languageController.userL1, + isL2List: false, + decorationText: + L10n.of(context).languageOfInstructionsLabel, + ), + const SizedBox(height: 24), + PLanguageDropdown( + languages: MatrixState + .pangeaController.pLanguageStore.targetOptions, + onChange: (val) => + _selectedTargetLanguage = val.langCode, + initialLanguage: MatrixState + .pangeaController.languageController.userL2, + decorationText: L10n.of(context).targetLanguageLabel, + isL2List: true, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _generateActivities(); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.lightbulb_outline), + const SizedBox(width: 8), + Text(l10n.generateActivitiesButton), + ], + ), + ), + ], ), - ], + ), ), - ], - ), - ), - ), - ); - } - - Widget _buildActivitiesView(L10n l10n) { - return ListView( - padding: const EdgeInsets.all(16), - children: _activities.asMap().entries.map((entry) { - final index = entry.key; - return ActivityPlanTile( - activity: _activities[index], - onLaunch: () => onLaunch(index), - onEdit: (val) { - setState(() { - _activities[index] = val; - }); - }, - ); - }).toList(), - ); - } -} - -class InnerSearchWidget extends StatelessWidget { - const InnerSearchWidget({ - super.key, - required TextEditingController searchController, - }) : _objectiveSearchController = searchController; - - final TextEditingController _objectiveSearchController; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 4, - right: 8, - left: 8, - ), - child: TextFormField( - controller: _objectiveSearchController, - textInputAction: TextInputAction.search, - decoration: InputDecoration( - isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - hintText: L10n.of(context).search, - icon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), + ), ); } } diff --git a/lib/pangea/activity_planner/activity_planner_settings_form.dart b/lib/pangea/activity_planner/activity_planner_settings_form.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/pangea/activity_planner/activity_planner_settings_form.dart @@ -0,0 +1 @@ + diff --git a/lib/pangea/activity_planner/activity_planner_settings_search_widget.dart b/lib/pangea/activity_planner/activity_planner_settings_search_widget.dart new file mode 100644 index 000000000..8b8d4da3a --- /dev/null +++ b/lib/pangea/activity_planner/activity_planner_settings_search_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class ActivityPlannerSettingsSearchWidget extends StatelessWidget { + const ActivityPlannerSettingsSearchWidget({ + super.key, + required TextEditingController searchController, + }) : _objectiveSearchController = searchController; + + final TextEditingController _objectiveSearchController; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 4, + right: 8, + left: 8, + ), + child: TextFormField( + controller: _objectiveSearchController, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + hintText: L10n.of(context).search, + icon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_planner/bookmarked_activities_repo.dart b/lib/pangea/activity_planner/bookmarked_activities_repo.dart new file mode 100644 index 000000000..5bf5192b6 --- /dev/null +++ b/lib/pangea/activity_planner/bookmarked_activities_repo.dart @@ -0,0 +1,39 @@ +import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:uuid/uuid.dart'; + +class BookmarkedActivitiesRepo { + static final GetStorage _storage = GetStorage('bookmarked_activities'); + static const Uuid _uuid = Uuid(); + + /// save an activity to the list of bookmarked activities + /// returns the activity with a bookmarkId + static Future save(ActivityPlanModel activity) async { + activity.bookmarkId ??= _uuid.v4(); + + await _storage.write( + activity.bookmarkId!, + activity.toJson(), + ); + + //now it has a bookmarkId + return activity; + } + + static Future remove(String bookmarkId) => _storage.remove(bookmarkId); + + static bool isBookmarked(ActivityPlanModel activity) { + return activity.bookmarkId != null && + _storage.read(activity.bookmarkId!) != null; + } + + static List get() { + final list = _storage.getValues(); + + if (list == null) return []; + + return (list as Iterable) + .map((json) => ActivityPlanModel.fromJson(json)) + .toList(); + } +} diff --git a/lib/pangea/activity_planner/media_enum.dart b/lib/pangea/activity_planner/media_enum.dart index 95e029235..3e1e11a3c 100644 --- a/lib/pangea/activity_planner/media_enum.dart +++ b/lib/pangea/activity_planner/media_enum.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; enum MediaEnum { @@ -11,7 +10,7 @@ enum MediaEnum { extension MediaEnumExtension on MediaEnum { //fromString - static MediaEnum fromString(String value) { + MediaEnum fromString(String value) { switch (value) { case 'images': return MediaEnum.images; diff --git a/lib/pangea/activity_planner/suggestion_form_field.dart b/lib/pangea/activity_planner/suggestion_form_field.dart new file mode 100644 index 000000000..8f4dc4eab --- /dev/null +++ b/lib/pangea/activity_planner/suggestion_form_field.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; +import 'package:flutter/material.dart'; + +class SuggestionFormField extends StatelessWidget { + final Future> suggestions; + final String? Function(String?)? validator; + final String label; + final String placeholder; + final void Function(String) onSelected; + final String? initialValue; + final TextEditingController controller; + + const SuggestionFormField({ + super.key, + required this.suggestions, + required this.placeholder, + this.validator, + required this.label, + required this.onSelected, + required this.initialValue, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return Autocomplete( + initialValue: + initialValue != null ? TextEditingValue(text: initialValue!) : null, + optionsBuilder: (TextEditingValue textEditingValue) async { + return (await suggestions) + .where((ActivitySettingResponseSchema option) { + return option.name + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }).map((ActivitySettingResponseSchema e) => e.name); + }, + onSelected: onSelected, + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + textEditingController.value = controller.value; + return TextFormField( + controller: textEditingController, + focusNode: focusNode, + decoration: InputDecoration( + labelText: label, + hintText: placeholder, + ), + validator: validator, + ); + }, + ); + } +} diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index 6a49117f5..18f0ee0ee 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -2,11 +2,6 @@ import 'dart:async'; import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; - -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/analytics/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/analytics/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; @@ -30,6 +25,10 @@ import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller. import 'package:fluffychat/pangea/user/controllers/permissions_controller.dart'; import 'package:fluffychat/pangea/user/controllers/user_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../../../config/app_config.dart'; import '../../choreographer/controllers/it_feedback_controller.dart'; import '../utils/firebase_analytics.dart'; @@ -186,7 +185,7 @@ class PangeaController { final List botDMs = []; for (final room in matrixState.client.rooms) { - if (await room.isBotDM) { + if (room.isBotDM) { botDMs.add(room); } } diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index b73f895ce..771c1fe25 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -4,18 +4,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:html_unescape/html_unescape.dart'; -import 'package:matrix/matrix.dart' as matrix; -import 'package:matrix/matrix.dart'; -import 'package:matrix/src/utils/markdown.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/analytics/models/constructs_event.dart'; import 'package:fluffychat/pangea/analytics/models/constructs_model.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; @@ -29,6 +19,15 @@ import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/pangea/spaces/models/space_model.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:html_unescape/html_unescape.dart'; +import 'package:matrix/matrix.dart' as matrix; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/markdown.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../choreographer/models/choreo_record.dart'; import '../events/constants/pangea_event_types.dart'; import '../events/models/representation_content_model.dart'; @@ -195,7 +194,7 @@ extension PangeaRoom on Room { Future get botIsInRoom async => await _botIsInRoom; - Future get isBotDM async => await _isBotDM; + bool get isBotDM => _isBotDM; // bool get isLocked => _isLocked; diff --git a/lib/pangea/extensions/room_information_extension.dart b/lib/pangea/extensions/room_information_extension.dart index e00f1f7fc..d20936571 100644 --- a/lib/pangea/extensions/room_information_extension.dart +++ b/lib/pangea/extensions/room_information_extension.dart @@ -41,7 +41,7 @@ extension RoomInformationRoomExtension on Room { ); } - Future get _isBotDM async => botOptions?.mode == BotMode.directChat; + bool get _isBotDM => botOptions?.mode == BotMode.directChat; bool _isAnalyticsRoomOfUser(String userId) => isAnalyticsRoom && isMadeByUser(userId);