feat(activity_planner): full draft done (#1542)
* feat(activity_planner): mvp done * refactor(activity_planner): move launch icon button to file * dev: dart formatting --------- Co-authored-by: ggurdin <ggurdin@gmail.com> Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
This commit is contained in:
parent
cbf9bd22ee
commit
b81f3841f8
20 changed files with 1043 additions and 47 deletions
|
|
@ -4749,5 +4749,23 @@
|
|||
},
|
||||
"notInClass": "Not in a class!",
|
||||
"noClassCode": "No class code!",
|
||||
"chooseCorrectLabel": "Choose the correct label"
|
||||
"chooseCorrectLabel": "Choose the correct label",
|
||||
"activityPlannerTitle": "Activity Planner",
|
||||
"topicLabel": "Topic",
|
||||
"topicPlaceholder": "Choose a topic...",
|
||||
"modeLabel": "Mode",
|
||||
"modePlaceholder": "Choose a mode...",
|
||||
"learningObjectiveLabel": "Learning Objective",
|
||||
"learningObjectivePlaceholder": "Choose a learning objective...",
|
||||
"mediaLabel": "Media students should share",
|
||||
"languageOfInstructionsLabel": "Language of activity instructions",
|
||||
"targetLanguageLabel": "Target language",
|
||||
"cefrLevelLabel": "CEFR Level",
|
||||
"generateActivitiesButton": "Generate Activities",
|
||||
"launchActivityButton": "Launch Activity",
|
||||
"image": "Image",
|
||||
"video": "Video",
|
||||
"voiceMessage": "Voice message",
|
||||
"nan": "Not applicable",
|
||||
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!"
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
|
|||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_page_launch_icon_button.dart';
|
||||
import 'package:fluffychat/pangea/analytics/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics/widgets/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
|
||||
|
|
@ -127,6 +128,7 @@ class ChatView extends StatelessWidget {
|
|||
context.go('/rooms/${controller.room.id}/search');
|
||||
},
|
||||
),
|
||||
ActivityPlanPageLaunchIconButton(controller: controller),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
tooltip: L10n.of(context).chatDetails,
|
||||
|
|
|
|||
56
lib/pangea/activity_planner/activity_mode_list_repo.dart
Normal file
56
lib/pangea/activity_planner/activity_mode_list_repo.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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 '../common/network/requests.dart';
|
||||
|
||||
class ActivityModeListRepo {
|
||||
static final GetStorage _modeListStorage = GetStorage('mode_list_storage');
|
||||
|
||||
static void set(
|
||||
ActivitySettingRequestSchema request,
|
||||
List<ActivitySettingResponseSchema> response,
|
||||
) {
|
||||
_modeListStorage.write(
|
||||
request.storageKey,
|
||||
response.map((e) => e.toJson()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
|
||||
return List<ActivitySettingResponseSchema>.from(
|
||||
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<ActivitySettingResponseSchema>> get(
|
||||
ActivitySettingRequestSchema request,
|
||||
) async {
|
||||
final cachedJson = _modeListStorage.read(request.storageKey);
|
||||
if (cachedJson != null) {
|
||||
return ActivityModeListRepo.fromJson(cachedJson);
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.activityModeList,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = ActivityModeListRepo.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
122
lib/pangea/activity_planner/activity_plan_generation_repo.dart
Normal file
122
lib/pangea/activity_planner/activity_plan_generation_repo.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
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<String, dynamic> 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<String> activityPlans;
|
||||
|
||||
ActivityPlanResponse({required this.activityPlans});
|
||||
|
||||
factory ActivityPlanResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityPlanResponse(
|
||||
activityPlans: List<String>.from(json['activity_plans']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity_plans': activityPlans,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityPlanGenerationRepo {
|
||||
static final GetStorage _activityPlanStorage =
|
||||
GetStorage('activity_plan_storage');
|
||||
|
||||
static void set(ActivityPlanRequest request, ActivityPlanResponse response) {
|
||||
_activityPlanStorage.write(request.storageKey, response.toJson());
|
||||
}
|
||||
|
||||
static Future<ActivityPlanResponse> get(ActivityPlanRequest request) async {
|
||||
final cachedJson = _activityPlanStorage.read(request.storageKey);
|
||||
if (cachedJson != null) {
|
||||
final cached = ActivityPlanResponse.fromJson(cachedJson);
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.activityPlanGeneration,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = ActivityPlanResponse.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
|
||||
|
||||
class ActivityPlanPageLaunchIconButton extends StatelessWidget {
|
||||
const ActivityPlanPageLaunchIconButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final ChatController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.event_note_outlined),
|
||||
tooltip: L10n.of(context).activityPlannerTitle,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ActivityPlannerPage(room: controller.room),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
93
lib/pangea/activity_planner/activity_plan_tile.dart
Normal file
93
lib/pangea/activity_planner/activity_plan_tile.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
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<String> onEdit;
|
||||
|
||||
const ActivityPlanTile({
|
||||
super.key,
|
||||
required this.activity,
|
||||
required this.onLaunch,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivityPlanTileState createState() => ActivityPlanTileState();
|
||||
}
|
||||
|
||||
class ActivityPlanTileState extends State<ActivityPlanTile> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
466
lib/pangea/activity_planner/activity_planner_page.dart
Normal file
466
lib/pangea/activity_planner/activity_planner_page.dart
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
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_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/learning_objective_list_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart';
|
||||
import 'package:fluffychat/pangea/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
|
||||
|
||||
enum _PageMode {
|
||||
settings,
|
||||
activities,
|
||||
}
|
||||
|
||||
class ActivityPlannerPage extends StatefulWidget {
|
||||
final Room room;
|
||||
const ActivityPlannerPage({super.key, required this.room});
|
||||
|
||||
@override
|
||||
ActivityPlannerPageState createState() => ActivityPlannerPageState();
|
||||
}
|
||||
|
||||
class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
/// Index of the content to display
|
||||
_PageMode _pageMode = _PageMode.settings;
|
||||
|
||||
/// Selected values from the form
|
||||
String? _selectedTopic;
|
||||
String? _selectedMode;
|
||||
String? _selectedObjective;
|
||||
MediaEnum _selectedMedia = MediaEnum.nan;
|
||||
String? _selectedLanguageOfInstructions;
|
||||
String? _selectedTargetLanguage;
|
||||
int? _selectedCefrLevel;
|
||||
|
||||
/// fetch data from repos
|
||||
List<ActivitySettingResponseSchema> _topicItems = [];
|
||||
List<ActivitySettingResponseSchema> _modeItems = [];
|
||||
List<ActivitySettingResponseSchema> _objectiveItems = [];
|
||||
|
||||
/// List of activities generated by the system
|
||||
List<String> _activities = [];
|
||||
|
||||
final _topicSearchController = TextEditingController();
|
||||
final _objectiveSearchController = TextEditingController();
|
||||
|
||||
final List<TextEditingController> _activityControllers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_topicSearchController.dispose();
|
||||
_objectiveSearchController.dispose();
|
||||
disposeAndClearActivityControllers();
|
||||
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<void> _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;
|
||||
});
|
||||
}
|
||||
|
||||
// send the activity as a message to the room
|
||||
Future<void> 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
|
||||
late List<PangeaToken> 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);
|
||||
}
|
||||
|
||||
final eventId = await widget.room.pangeaSendTextEvent(
|
||||
_activities[index],
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
originalSent: PangeaRepresentation(
|
||||
langCode: _selectedLanguageOfInstructions!,
|
||||
text: _activities[index],
|
||||
originalSent: true,
|
||||
originalWritten: false,
|
||||
),
|
||||
tokensSent: PangeaMessageTokens(tokens: tokens),
|
||||
);
|
||||
|
||||
if (eventId == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.room.setPinnedEvents([eventId]);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> _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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _randomizeSelections() {
|
||||
setState(() {
|
||||
_selectedTopic = (_topicItems..shuffle()).first.name;
|
||||
_selectedObjective = (_objectiveItems..shuffle()).first.name;
|
||||
_selectedMode = (_modeItems..shuffle()).first.name;
|
||||
});
|
||||
}
|
||||
|
||||
// Add validation logic
|
||||
String? _validateNotNull(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return L10n.of(context).interactiveTranslatorRequired;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@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: Text(l10n.activityPlannerTitle),
|
||||
),
|
||||
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<String>(
|
||||
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<String>(
|
||||
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<String>(
|
||||
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<MediaEnum>(
|
||||
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: _randomizeSelections,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _generateActivities,
|
||||
child: 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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 '../common/network/requests.dart';
|
||||
|
||||
class LearningObjectiveListRepo {
|
||||
static final GetStorage _objectiveListStorage =
|
||||
GetStorage('objective_list_storage');
|
||||
|
||||
static void set(
|
||||
ActivitySettingRequestSchema request,
|
||||
List<ActivitySettingResponseSchema> response,
|
||||
) {
|
||||
_objectiveListStorage.write(
|
||||
request.storageKey,
|
||||
response.map((e) => e.toJson()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
|
||||
return List<ActivitySettingResponseSchema>.from(
|
||||
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<ActivitySettingResponseSchema>> get(
|
||||
ActivitySettingRequestSchema request,
|
||||
) async {
|
||||
final cachedJson = _objectiveListStorage.read(request.storageKey);
|
||||
if (cachedJson != null) {
|
||||
return LearningObjectiveListRepo.fromJson(cachedJson);
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.objectiveList,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = LearningObjectiveListRepo.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
37
lib/pangea/activity_planner/list_request_schema.dart
Normal file
37
lib/pangea/activity_planner/list_request_schema.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
class ActivitySettingRequestSchema {
|
||||
final String langCode;
|
||||
|
||||
ActivitySettingRequestSchema({required this.langCode});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lang_code': langCode,
|
||||
};
|
||||
}
|
||||
|
||||
String get storageKey => 'topic_list-$langCode';
|
||||
}
|
||||
|
||||
class ActivitySettingResponseSchema {
|
||||
final String defaultName;
|
||||
final String name;
|
||||
|
||||
ActivitySettingResponseSchema({
|
||||
required this.defaultName,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory ActivitySettingResponseSchema.fromJson(Map<String, dynamic> json) {
|
||||
return ActivitySettingResponseSchema(
|
||||
defaultName: json['default_name'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'default_name': defaultName,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
}
|
||||
55
lib/pangea/activity_planner/media_enum.dart
Normal file
55
lib/pangea/activity_planner/media_enum.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum MediaEnum {
|
||||
images,
|
||||
videos,
|
||||
voiceMessages,
|
||||
nan,
|
||||
}
|
||||
|
||||
extension MediaEnumExtension on MediaEnum {
|
||||
//fromString
|
||||
static MediaEnum fromString(String value) {
|
||||
switch (value) {
|
||||
case 'images':
|
||||
return MediaEnum.images;
|
||||
case 'videos':
|
||||
return MediaEnum.videos;
|
||||
case 'voice_messages':
|
||||
return MediaEnum.voiceMessages;
|
||||
case 'nan':
|
||||
return MediaEnum.nan;
|
||||
default:
|
||||
return MediaEnum.nan;
|
||||
}
|
||||
}
|
||||
|
||||
String get string {
|
||||
switch (this) {
|
||||
case MediaEnum.images:
|
||||
return 'images';
|
||||
case MediaEnum.videos:
|
||||
return 'videos';
|
||||
case MediaEnum.voiceMessages:
|
||||
return 'voice_messages';
|
||||
case MediaEnum.nan:
|
||||
return 'nan';
|
||||
}
|
||||
}
|
||||
|
||||
//toDisplayCopyUsingL10n
|
||||
String toDisplayCopyUsingL10n(BuildContext context) {
|
||||
switch (this) {
|
||||
case MediaEnum.images:
|
||||
return L10n.of(context).image;
|
||||
case MediaEnum.videos:
|
||||
return L10n.of(context).video;
|
||||
case MediaEnum.voiceMessages:
|
||||
return L10n.of(context).voiceMessage;
|
||||
case MediaEnum.nan:
|
||||
return L10n.of(context).nan;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
lib/pangea/activity_planner/topic_list_repo.dart
Normal file
56
lib/pangea/activity_planner/topic_list_repo.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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 '../common/network/requests.dart';
|
||||
|
||||
class TopicListRepo {
|
||||
static final GetStorage _topicListStorage = GetStorage('topic_list_storage');
|
||||
|
||||
static void set(
|
||||
ActivitySettingRequestSchema request,
|
||||
List<ActivitySettingResponseSchema> response,
|
||||
) {
|
||||
_topicListStorage.write(
|
||||
request.storageKey,
|
||||
response.map((e) => e.toJson()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
|
||||
return List<ActivitySettingResponseSchema>.from(
|
||||
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<ActivitySettingResponseSchema>> get(
|
||||
ActivitySettingRequestSchema request,
|
||||
) async {
|
||||
final cachedJson = _topicListStorage.read(request.storageKey);
|
||||
if (cachedJson != null) {
|
||||
return TopicListRepo.fromJson(cachedJson);
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.topicList,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = TopicListRepo.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ class LanguageLevelDropdown extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField2(
|
||||
decoration: InputDecoration(labelText: L10n.of(context).cefrLevelLabel),
|
||||
hint: Text(
|
||||
L10n.of(context).selectLanguageLevel,
|
||||
overflow: TextOverflow.clip,
|
||||
|
|
|
|||
|
|
@ -87,8 +87,11 @@ class ModelKey {
|
|||
/// This will help us know to omit the message from notifications,
|
||||
/// bot responses, etc. It will also help use find the message if
|
||||
/// we want to gather user edits for LLM fine-tuning.
|
||||
/// @ggurdin: Maybe this not the way to do this and we should be using
|
||||
/// something built in to matrix? should talk about this
|
||||
static const String messageTags = "p.tag";
|
||||
static const String messageTagMorphEdit = "morph_edit";
|
||||
static const String messageTagActivityPlan = "activity_plan";
|
||||
|
||||
static const String baseDefinition = "base_definition";
|
||||
static const String targetDefinition = "target_definition";
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ class PApiUrls {
|
|||
|
||||
static String lemmaDictionary = "${PApiUrls.choreoEndpoint}/lemma_definition";
|
||||
|
||||
static String activityPlanGeneration =
|
||||
"${PApiUrls.choreoEndpoint}/activity_plan";
|
||||
static String activityModeList = "${PApiUrls.choreoEndpoint}/modes";
|
||||
static String objectiveList = "${PApiUrls.choreoEndpoint}/objectives";
|
||||
static String topicList = "${PApiUrls.choreoEndpoint}/topics";
|
||||
|
||||
///-------------------------------- revenue cat --------------------------
|
||||
static String rcAppsChoreo = "${PApiUrls.subscriptionEndpoint}/app_ids";
|
||||
static String rcProductsChoreo =
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ enum InstructionsEnum {
|
|||
clickBestOption,
|
||||
unlockedLanguageTools,
|
||||
lemmaMeaning,
|
||||
activityPlannerOverview,
|
||||
}
|
||||
|
||||
extension InstructionsEnumExtension on InstructionsEnum {
|
||||
|
|
@ -36,6 +37,7 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return l10n.tooltipInstructionsTitle;
|
||||
case InstructionsEnum.missingVoice:
|
||||
return l10n.missingVoiceTitle;
|
||||
case InstructionsEnum.activityPlannerOverview:
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
case InstructionsEnum.speechToText:
|
||||
case InstructionsEnum.l1Translation:
|
||||
|
|
@ -83,6 +85,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return l10n.unlockedLanguageTools;
|
||||
case InstructionsEnum.lemmaMeaning:
|
||||
return l10n.lemmaMeaningInstructionsBody;
|
||||
case InstructionsEnum.activityPlannerOverview:
|
||||
return l10n.activityPlannerOverviewInstructionsBody;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ Future<void> pLanguageDialog(
|
|||
selectedSourceLanguage ?? LanguageModel.unknown,
|
||||
languages: pangeaController.pLanguageStore.baseOptions,
|
||||
isL2List: false,
|
||||
decorationText: L10n.of(context).myBaseLanguage,
|
||||
),
|
||||
PQuestionContainer(
|
||||
title: L10n.of(context).whatLanguageYouWantToLearn,
|
||||
|
|
@ -76,6 +77,7 @@ Future<void> pLanguageDialog(
|
|||
initialLanguage: selectedTargetLanguage,
|
||||
languages: pangeaController.pLanguageStore.targetOptions,
|
||||
isL2List: true,
|
||||
decorationText: L10n.of(context).iWantToLearn,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart';
|
||||
|
|
@ -15,6 +15,7 @@ class PLanguageDropdown extends StatefulWidget {
|
|||
final Function(LanguageModel) onChange;
|
||||
final bool showMultilingual;
|
||||
final bool isL2List;
|
||||
final String decorationText;
|
||||
final String? error;
|
||||
|
||||
const PLanguageDropdown({
|
||||
|
|
@ -23,7 +24,8 @@ class PLanguageDropdown extends StatefulWidget {
|
|||
required this.onChange,
|
||||
required this.initialLanguage,
|
||||
this.showMultilingual = false,
|
||||
required this.isL2List,
|
||||
required this.decorationText,
|
||||
this.isL2List = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
|
|
@ -63,52 +65,30 @@ class _PLanguageDropdownState extends State<PLanguageDropdown> {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(36)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: DropdownButton<LanguageModel>(
|
||||
hint: Row(
|
||||
children: [
|
||||
const Icon(Icons.language_outlined),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
widget.isL2List
|
||||
? L10n.of(context).iWantToLearn
|
||||
: L10n.of(context).myBaseLanguage,
|
||||
),
|
||||
],
|
||||
),
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
underline: Container(),
|
||||
items: [
|
||||
if (widget.showMultilingual)
|
||||
DropdownMenuItem(
|
||||
value: LanguageModel.multiLingual(context),
|
||||
child: LanguageDropDownEntry(
|
||||
languageModel: LanguageModel.multiLingual(context),
|
||||
isL2List: widget.isL2List,
|
||||
),
|
||||
),
|
||||
...sortedLanguages.map(
|
||||
(languageModel) => DropdownMenuItem(
|
||||
value: languageModel,
|
||||
child: LanguageDropDownEntry(
|
||||
languageModel: languageModel,
|
||||
isL2List: widget.isL2List,
|
||||
),
|
||||
DropdownButtonFormField2<LanguageModel>(
|
||||
decoration: InputDecoration(labelText: widget.decorationText),
|
||||
isExpanded: true,
|
||||
items: [
|
||||
if (widget.showMultilingual)
|
||||
DropdownMenuItem(
|
||||
value: LanguageModel.multiLingual(context),
|
||||
child: LanguageDropDownEntry(
|
||||
languageModel: LanguageModel.multiLingual(context),
|
||||
isL2List: widget.isL2List,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) => widget.onChange(value!),
|
||||
value: widget.initialLanguage,
|
||||
),
|
||||
...sortedLanguages.map(
|
||||
(languageModel) => DropdownMenuItem(
|
||||
value: languageModel,
|
||||
child: LanguageDropDownEntry(
|
||||
languageModel: languageModel,
|
||||
isL2List: widget.isL2List,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) => widget.onChange(value!),
|
||||
value: widget.initialLanguage,
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class UserSettingsView extends StatelessWidget {
|
|||
initialLanguage: controller.selectedTargetLanguage,
|
||||
isL2List: true,
|
||||
error: controller.selectedLanguageError,
|
||||
decorationText: L10n.of(context).iWantToLearn,
|
||||
),
|
||||
),
|
||||
if (controller.isSSOSignup)
|
||||
|
|
|
|||
|
|
@ -239,6 +239,9 @@ class LanguageSelectionRow extends StatelessWidget {
|
|||
initialLanguage: isSource
|
||||
? controller.sourceLanguageSearch
|
||||
: controller.targetLanguageSearch,
|
||||
decorationText: isSource
|
||||
? L10n.of(context).myBaseLanguage
|
||||
: L10n.of(context).iWantToLearn,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue