feat: featured activities page and new activity planner navigation (#2242)
This commit is contained in:
parent
4a20a1fe5b
commit
66ac13f3bf
11 changed files with 585 additions and 389 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
66
lib/pangea/activity_planner/bookmarked_activity_list.dart
Normal file
66
lib/pangea/activity_planner/bookmarked_activity_list.dart
Normal 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(() {}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() {}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
193
lib/pangea/activity_planner/new_activity_form.dart
Normal file
193
lib/pangea/activity_planner/new_activity_form.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@ class ActivitySuggestionCarouselState
|
|||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
padding: 0.0,
|
||||
onChange: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue