feat: featured activities page and new activity planner navigation (#2242)

This commit is contained in:
ggurdin 2025-03-27 14:20:31 -04:00 committed by GitHub
parent 4a20a1fe5b
commit 66ac13f3bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 585 additions and 389 deletions

View file

@ -19,6 +19,7 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
class ActivityPlanCard extends StatefulWidget {
final ActivityPlanModel activity;
final Room? room;
final VoidCallback onChange;
final ValueChanged<ActivityPlanModel> onEdit;
final double maxWidth;
final String? avatarURL;
@ -28,6 +29,7 @@ class ActivityPlanCard extends StatefulWidget {
super.key,
required this.activity,
required this.room,
required this.onChange,
required this.onEdit,
this.maxWidth = 400,
this.avatarURL,
@ -47,6 +49,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
final TextEditingController _newVocabController = TextEditingController();
final FocusNode _vocabFocusNode = FocusNode();
String? _avatarURL;
Uint8List? _avatar;
String? _filename;
@ -60,6 +63,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
_instructionsController =
TextEditingController(text: _tempActivity.instructions);
_filename = widget.initialFilename;
_avatarURL = widget.avatarURL ?? widget.activity.imageURL;
}
static const double itemPadding = 12;
@ -81,13 +85,10 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
learningObjective: _learningObjectiveController.text,
instructions: _instructionsController.text,
vocab: _tempActivity.vocab,
imageURL: _avatarURL,
);
final activityWithBookmarkId = await _addBookmark(updatedActivity);
// need to save in the repo as well
widget.onEdit(activityWithBookmarkId);
widget.onEdit(updatedActivity);
setState(() {
_isEditing = false;
});
@ -99,16 +100,22 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
ErrorHandler.logError(e: e, s: stack, data: activity.toJson());
return activity; // Return the original activity in case of error
}).whenComplete(() {
setState(() {});
if (mounted) {
setState(() {});
widget.onChange();
}
});
Future<void> _removeBookmark() =>
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId!)
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId)
.catchError((e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson());
}).whenComplete(() {
setState(() {});
if (mounted) {
setState(() {});
widget.onChange();
}
});
void _addVocab() {
@ -148,7 +155,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
await widget.room?.sendActivityPlan(
widget.activity,
avatar: _avatar,
avatarURL: widget.avatarURL,
avatarURL: _avatarURL,
filename: _filename,
);
@ -179,12 +186,12 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
clipBehavior: Clip.hardEdge,
alignment: Alignment.center,
child: widget.avatarURL != null || _avatar != null
child: _avatarURL != null || _avatar != null
? ClipRRect(
child: _avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: widget.avatarURL!,
imageUrl: _avatarURL!,
placeholder: (context, url) {
return const Center(
child: CircularProgressIndicator(),

View file

@ -10,7 +10,6 @@ class ActivityPlanModel {
String instructions;
List<Vocab> vocab;
String? imageURL;
String? bookmarkId;
ActivityPlanModel({
required this.req,
@ -19,7 +18,6 @@ class ActivityPlanModel {
required this.instructions,
required this.vocab,
this.imageURL,
this.bookmarkId,
});
factory ActivityPlanModel.fromJson(Map<String, dynamic> json) {
@ -31,7 +29,6 @@ class ActivityPlanModel {
vocab: List<Vocab>.from(
json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)),
),
bookmarkId: json[ModelKey.activityPlanBookmarkId],
imageURL: json[ModelKey.activityPlanImageURL],
);
}
@ -67,10 +64,6 @@ class ActivityPlanModel {
return markdown;
}
bool get isBookmarked {
return bookmarkId != null;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
@ -90,8 +83,15 @@ class ActivityPlanModel {
title.hashCode ^
learningObjective.hashCode ^
instructions.hashCode ^
Object.hashAll(vocab) ^
bookmarkId.hashCode;
Object.hashAll(vocab);
String get bookmarkId {
return (title.hashCode ^
learningObjective.hashCode ^
instructions.hashCode ^
Object.hashAll(vocab))
.toString();
}
}
class Vocab {

View file

@ -2,33 +2,30 @@ import 'dart:math';
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_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.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/suggestion_form_field.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/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.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/enums/language_level_type_enum.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';
enum _PageMode {
enum PageMode {
settings,
generatedActivities,
featuredActivities,
savedActivities,
}
@ -41,16 +38,14 @@ class ActivityPlannerPage extends StatefulWidget {
}
class ActivityPlannerPageState extends State<ActivityPlannerPage> {
final _formKey = GlobalKey<FormState>();
final formKey = GlobalKey<FormState>();
PageMode pageMode = PageMode.featuredActivities;
/// Index of the content to display
_PageMode _pageMode = _PageMode.settings;
MediaEnum _selectedMedia = MediaEnum.nan;
String? _selectedLanguageOfInstructions;
String? _selectedTargetLanguage;
LanguageLevelTypeEnum? _selectedCefrLevel;
int? _selectedNumberOfParticipants;
MediaEnum selectedMedia = MediaEnum.nan;
String? selectedLanguageOfInstructions;
String? selectedTargetLanguage;
LanguageLevelTypeEnum? selectedCefrLevel;
int? selectedNumberOfParticipants;
List<String> activities = [];
@ -67,36 +62,35 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
}
if (_initialActivity == null) {
_selectedLanguageOfInstructions =
selectedLanguageOfInstructions =
MatrixState.pangeaController.languageController.userL1?.langCode;
_selectedTargetLanguage =
selectedTargetLanguage =
MatrixState.pangeaController.languageController.userL2?.langCode;
_selectedCefrLevel = LanguageLevelTypeEnum.a1;
_selectedNumberOfParticipants =
selectedCefrLevel = LanguageLevelTypeEnum.a1;
selectedNumberOfParticipants =
max(room?.getParticipants().length ?? 1, 1);
} else {
_selectedMedia = _initialActivity!.req.media;
_selectedLanguageOfInstructions =
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;
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();
final topicController = TextEditingController();
final objectiveController = TextEditingController();
final modeController = TextEditingController();
@override
void dispose() {
_topicController.dispose();
_objectiveController.dispose();
_modeController.dispose();
topicController.dispose();
objectiveController.dispose();
modeController.dispose();
super.dispose();
}
@ -106,27 +100,51 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
LanguageKeys.defaultLanguage,
);
Future<List<ActivitySettingResponseSchema>> get _topicItems =>
Future<List<ActivitySettingResponseSchema>> get topicItems =>
TopicListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get modeItems =>
ActivityModeListRepo.get(req);
Future<List<ActivitySettingResponseSchema>> get _objectiveItems =>
Future<List<ActivitySettingResponseSchema>> get objectiveItems =>
LearningObjectiveListRepo.get(req);
Future<void> _generateActivities() async {
_pageMode = _PageMode.generatedActivities;
setState(() {});
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;
final topics = await topicItems;
return (topics..shuffle()).first.name;
}
Future<String> _randomObjective() async {
final objectives = await _objectiveItems;
final objectives = await objectiveItems;
return (objectives..shuffle()).first.name;
}
@ -135,264 +153,103 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
return (modes..shuffle()).first.name;
}
void _randomizeSelections() async {
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;
topicController.text = selectedTopic;
objectiveController.text = selectedObjective;
modeController.text = selectedMode;
});
}
// Add validation logic
String? _validateNotNull(String? value) {
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) {
final l10n = L10n.of(context);
return Scaffold(
appBar: AppBar(
leading: _pageMode == _PageMode.settings
? IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
)
: IconButton(
onPressed: () => setState(() => _pageMode = _PageMode.settings),
icon: const Icon(Icons.arrow_back),
),
title: _pageMode == _PageMode.savedActivities
? Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.bookmarks),
const SizedBox(width: 8),
Text(l10n.myBookmarkedActivities),
],
),
)
: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.event_note_outlined),
const SizedBox(width: 8),
Text(l10n.activityPlannerTitle),
],
),
),
actions: [
Tooltip(
message: l10n.myBookmarkedActivities,
child: IconButton(
onPressed: () =>
setState(() => _pageMode = _PageMode.savedActivities),
icon: const Icon(Icons.bookmarks),
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,
controller: this,
);
break;
case PageMode.featuredActivities:
body = const Expanded(
child: SingleChildScrollView(
child: ActivitySuggestionsArea(
scrollDirection: Axis.vertical,
),
),
],
);
break;
}
return Scaffold(
appBar: ActivityPlannerPageAppBar(
pageMode: pageMode,
setPageMode: _setPageMode,
),
body: _pageMode != _PageMode.settings
? ActivityListView(
room: room,
activityPlanRequest: _PageMode.savedActivities == _pageMode
? null
: ActivityPlanRequest(
topic: _topicController.text,
mode: _modeController.text,
objective: _objectiveController.text,
media: _selectedMedia,
languageOfInstructions: _selectedLanguageOfInstructions!,
targetLanguage: _selectedTargetLanguage!,
cefrLevel: _selectedCefrLevel!,
numberOfParticipants: _selectedNumberOfParticipants!,
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"),
),
],
),
controller: this,
)
: 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,
controller: _topicController,
),
const SizedBox(height: 24),
SuggestionFormField(
suggestions: _objectiveItems,
validator: _validateNotNull,
label: l10n.learningObjectiveLabel,
placeholder:
l10n.learningObjectivePlaceholder,
controller: _objectiveController,
),
const SizedBox(height: 24),
SuggestionFormField(
suggestions: modeItems,
validator: _validateNotNull,
label: l10n.modeLabel,
placeholder: l10n.modePlaceholder,
controller: _modeController,
),
],
),
),
const SizedBox(width: 4),
Column(
children: [
IconButton(
icon: const Icon(Icons.shuffle),
onPressed: _randomizeSelections,
),
],
),
],
),
const SizedBox(height: 24),
DropdownButtonFormField2<MediaEnum>(
customButton: CustomDropdownTextButton(
text: _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: _selectedMedia == e,
),
),
)
.toList(),
onChanged: (val) {
setState(() => _selectedMedia = val ?? MediaEnum.nan);
},
value: _selectedMedia,
),
const SizedBox(height: 24),
LanguageLevelDropdown(
initialLevel: _selectedCefrLevel,
onChanged: (val) =>
setState(() => _selectedCefrLevel = val),
),
const SizedBox(height: 24),
PLanguageDropdown(
languages: MatrixState
.pangeaController.pLanguageStore.baseOptions,
onChange: (val) => setState(
() => _selectedLanguageOfInstructions = val.langCode,
),
initialLanguage: _selectedLanguageOfInstructions != null
? PLanguageStore.byLangCode(
_selectedLanguageOfInstructions!,
)
: MatrixState
.pangeaController.languageController.userL1,
isL2List: false,
decorationText:
L10n.of(context).languageOfInstructionsLabel,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages: MatrixState
.pangeaController.pLanguageStore.targetOptions,
onChange: (val) => setState(
() => _selectedTargetLanguage = val.langCode,
),
initialLanguage: _selectedTargetLanguage != null
? PLanguageStore.byLangCode(
_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) =>
_selectedNumberOfParticipants = int.tryParse(val),
initialValue: _selectedNumberOfParticipants?.toString(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onFieldSubmitted: (_) {
if (_formKey.currentState?.validate() ?? false) {
_generateActivities();
}
},
),
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),
],
),
),
],
),
),
],
),
),
body,
],
),
),
);
}
}

View file

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
class ActivityPlannerPageAppBar extends StatelessWidget
implements PreferredSizeWidget {
final PageMode pageMode;
final Function(PageMode) setPageMode;
const ActivityPlannerPageAppBar({
required this.pageMode,
required this.setPageMode,
super.key,
});
@override
Size get preferredSize => const Size.fromHeight(72);
@override
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),
),
title: pageMode == PageMode.savedActivities
? Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.bookmarks),
const SizedBox(width: 8),
Flexible(
child: Text(l10n.myBookmarkedActivities),
),
],
),
)
: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.event_note_outlined),
const SizedBox(width: 8),
Flexible(
child: Text(
l10n.activityPlannerTitle,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
actions: [
IconButton(
onPressed: () => setPageMode(PageMode.settings),
icon: const Icon(Icons.edit_outlined),
),
],
);
}
}

View file

@ -1,22 +1,17 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:get_storage/get_storage.dart';
import 'package:uuid/uuid.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
class BookmarkedActivitiesRepo {
static const Uuid _uuid = Uuid();
static final GetStorage _bookStorage = GetStorage('bookmarked_activities');
/// save an activity to the list of bookmarked activities
/// returns the activity with a bookmarkId
static Future<ActivityPlanModel> save(ActivityPlanModel activity) async {
activity.bookmarkId ??= _uuid.v4();
await _bookStorage.write(
activity.bookmarkId!,
activity.bookmarkId,
activity.toJson(),
);
@ -28,8 +23,7 @@ class BookmarkedActivitiesRepo {
_bookStorage.remove(bookmarkId);
static bool isBookmarked(ActivityPlanModel activity) {
return activity.bookmarkId != null &&
_bookStorage.read(activity.bookmarkId!) != null;
return _bookStorage.read(activity.bookmarkId) != null;
}
static List<ActivityPlanModel> get() {

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'activity_plan_card.dart';
class BookmarkedActivitiesList extends StatefulWidget {
final Room? room;
final ActivityPlannerPageState controller;
const BookmarkedActivitiesList({
super.key,
required this.room,
required this.controller,
});
@override
BookmarkedActivitiesListState createState() =>
BookmarkedActivitiesListState();
}
class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
List<ActivityPlanModel> get _bookmarkedActivities =>
BookmarkedActivitiesRepo.get();
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
if (_bookmarkedActivities.isEmpty) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 200),
child: Text(
l10n.noBookmarkedActivities,
textAlign: TextAlign.center,
),
),
);
}
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _bookmarkedActivities.length,
itemBuilder: (context, index) {
final activity = _bookmarkedActivities[index];
return ActivityPlanCard(
activity: activity,
room: widget.room,
onEdit: (updatedActivity) async {
await BookmarkedActivitiesRepo.remove(activity.bookmarkId);
await BookmarkedActivitiesRepo.save(updatedActivity);
setState(() {});
},
onChange: () => setState(() {}),
);
},
),
);
}
}

View file

@ -2,44 +2,31 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.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_request.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/bookmarked_activities_repo.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 ActivityListView extends StatefulWidget {
final Room? room;
/// if null, show saved activities
final ActivityPlanRequest? activityPlanRequest;
class GeneratedActivitiesList extends StatefulWidget {
final ActivityPlannerPageState controller;
const ActivityListView({
const GeneratedActivitiesList({
super.key,
required this.room,
required this.activityPlanRequest,
required this.controller,
});
@override
ActivityListViewState createState() => ActivityListViewState();
GeneratedActivitiesListState createState() => GeneratedActivitiesListState();
}
class ActivityListViewState extends State<ActivityListView> {
class GeneratedActivitiesListState extends State<GeneratedActivitiesList> {
List<ActivityPlanModel>? _activities;
List<ActivityPlanModel> get _bookmarkedActivities =>
BookmarkedActivitiesRepo.get();
bool _isLoading = true;
Object? _error;
@ -60,20 +47,17 @@ class ActivityListViewState extends State<ActivityListView> {
});
try {
if (widget.activityPlanRequest != null) {
final resp = await ActivityPlanGenerationRepo.get(
widget.activityPlanRequest!,
);
_activities = resp.activityPlans;
}
final resp = await ActivityPlanGenerationRepo.get(
widget.controller.planRequest,
);
_activities = resp.activityPlans;
} catch (e, s) {
_error = e;
ErrorHandler.logError(
e: e,
s: s,
data: {
'room': widget.room,
'activityPlanRequest': widget.activityPlanRequest,
'activityPlanRequest': widget.controller.planRequest,
},
);
} finally {
@ -81,30 +65,6 @@ class ActivityListViewState extends State<ActivityListView> {
}
}
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 (widget.activityPlanRequest != null && _activities != null) {
final activities = _activities;
activities?[index] = updatedActivity;
ActivityPlanGenerationRepo.set(
widget.activityPlanRequest!,
ActivityPlanResponse(activityPlans: _activities!),
);
}
setState(() {});
}
Future<ActivitySettingResponseSchema?> get _selectedMode async {
final modes = await widget.controller.modeItems;
return modes.firstWhereOrNull(
(element) =>
element.name.toLowerCase() ==
widget.activityPlanRequest?.mode.toLowerCase(),
);
}
Future<void> _setModeImageURL() async {
final mode = await _selectedMode;
if (mode == null) return;
@ -121,11 +81,38 @@ class ActivityListViewState extends State<ActivityListView> {
});
}
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 Center(child: CircularProgressIndicator());
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: CircularProgressIndicator()),
);
} else if (_error != null) {
return Center(
child: Column(
@ -140,37 +127,24 @@ class ActivityListViewState extends State<ActivityListView> {
],
),
);
} else if (widget.activityPlanRequest != null &&
(_activities == null || _activities!.isEmpty)) {
} else if (_activities == null || _activities!.isEmpty) {
return Center(child: Text(l10n.noDataFound));
} else if (widget.activityPlanRequest == null &&
(_bookmarkedActivities.isEmpty)) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 200),
child: Text(
l10n.noBookmarkedActivities,
textAlign: TextAlign.center,
),
),
);
} else {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: widget.activityPlanRequest == null
? _bookmarkedActivities.length
: _activities!.length,
itemBuilder: (context, index) {
return ActivityPlanCard(
activity: widget.activityPlanRequest == null
? _bookmarkedActivities[index]
: _activities![index],
room: widget.room,
onEdit: (updatedActivity) => _onEdit(index, updatedActivity),
avatarURL: _avatarURL,
initialFilename: _filename,
);
},
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

@ -0,0 +1,193 @@
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

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/activity_suggestions/activity_suggestion_card_row.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
@ -14,6 +15,8 @@ class ActivitySuggestionCard extends StatelessWidget {
final double height;
final double padding;
final VoidCallback onChange;
const ActivitySuggestionCard({
super.key,
required this.activity,
@ -21,11 +24,14 @@ class ActivitySuggestionCard extends StatelessWidget {
required this.width,
required this.height,
required this.padding,
required this.onChange,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isBookmarked = BookmarkedActivitiesRepo.isBookmarked(activity);
return Padding(
padding: EdgeInsets.all(padding),
child: PressableButton(
@ -131,6 +137,21 @@ class ActivitySuggestionCard extends StatelessWidget {
),
],
),
Positioned(
top: 4.0,
right: 4.0,
child: IconButton(
icon: Icon(
isBookmarked ? Icons.bookmark : Icons.bookmark_border,
),
onPressed: () => isBookmarked
? BookmarkedActivitiesRepo.remove(activity.bookmarkId)
.then((_) => onChange())
: BookmarkedActivitiesRepo.save(activity)
.then((_) => onChange()),
iconSize: 24.0,
),
),
],
),
),

View file

@ -197,6 +197,9 @@ class ActivitySuggestionCarouselState
width: _cardWidth,
height: _cardHeight,
padding: 0.0,
onChange: () {
if (mounted) setState(() {});
},
),
),
MouseRegion(

View file

@ -16,7 +16,8 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
import 'package:fluffychat/widgets/matrix.dart';
class ActivitySuggestionsArea extends StatefulWidget {
const ActivitySuggestionsArea({super.key});
final Axis? scrollDirection;
const ActivitySuggestionsArea({super.key, this.scrollDirection});
@override
ActivitySuggestionsAreaState createState() => ActivitySuggestionsAreaState();
}
@ -80,6 +81,9 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
width: cardWidth,
height: cardHeight,
padding: cardPadding,
onChange: () {
if (mounted) setState(() {});
},
);
})
.cast<Widget>()
@ -94,7 +98,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
),
);
return _isColumnMode
final scrollDirection = widget.scrollDirection ??
(_isColumnMode ? Axis.horizontal : Axis.vertical);
return scrollDirection == Axis.horizontal
? ConstrainedBox(
constraints: BoxConstraints(maxHeight: cardHeight + 36.0),
child: Scrollbar(