4450 find course button overflow with german l1 (#4488)
* fix: text overflow in add course page German buttons * feat: show warning popup when user selects course plan in new course page if user is already in a course with the same ID * use firstWhereOrNull for room search
This commit is contained in:
parent
288aee68eb
commit
2efd61bccd
5 changed files with 261 additions and 150 deletions
|
|
@ -5311,5 +5311,7 @@
|
|||
"numSavedActivities": "Number of saved activities",
|
||||
"saveActivityTitle": "Save activity",
|
||||
"saveActivityDesc": "Good job! Save this activity for later review and practice",
|
||||
"levelInfoTooltip": "Here you can see all the points you’ve earned and how!"
|
||||
"levelInfoTooltip": "Here you can see all the points you’ve earned and how!",
|
||||
"alreadyInCourseWithID": "You are already in a course with this plan. Do you want to create a course with the same plan, or go to the existing course?",
|
||||
"goToExistingCourse": "Go to existing course"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_filter.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plans_repo.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
mixin CourseSearchProvider<T extends StatefulWidget> on State<T> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
Map<String, CoursePlanModel> courses = {};
|
||||
LanguageModel? targetLanguageFilter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _loadCourses(),
|
||||
);
|
||||
}
|
||||
|
||||
CourseFilter get _filter {
|
||||
return CourseFilter(
|
||||
targetLanguage: targetLanguageFilter,
|
||||
);
|
||||
}
|
||||
|
||||
void setTargetLanguageFilter(LanguageModel? language, {bool reload = true}) {
|
||||
if (targetLanguageFilter?.langCodeShort == language?.langCodeShort) return;
|
||||
setState(() => targetLanguageFilter = language);
|
||||
if (reload) _loadCourses();
|
||||
}
|
||||
|
||||
Future<void> _loadCourses() async {
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
final resp = await CoursePlansRepo.searchByFilter(filter: _filter);
|
||||
courses = resp.coursePlans;
|
||||
if (courses.isEmpty) {
|
||||
ErrorHandler.logError(
|
||||
e: "No courses found",
|
||||
data: {
|
||||
'filter': _filter.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'filter': _filter.toJson(),
|
||||
},
|
||||
);
|
||||
error = e;
|
||||
} finally {
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
|
||||
|
||||
extension CoursePlanClientExtension on Client {
|
||||
Room? getRoomByCourseId(String courseId) => rooms.firstWhereOrNull(
|
||||
(room) => room.coursePlan?.uuid == courseId,
|
||||
);
|
||||
}
|
||||
|
|
@ -72,7 +72,12 @@ class AddCoursePage extends StatelessWidget {
|
|||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Text(L10n.of(context).joinCourseWithCode),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).joinCourseWithCode,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -93,7 +98,12 @@ class AddCoursePage extends StatelessWidget {
|
|||
size: 24.0,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
Text(L10n.of(context).joinPublicCourse),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).joinPublicCourse,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -118,7 +128,12 @@ class AddCoursePage extends StatelessWidget {
|
|||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
Text(L10n.of(context).startOwn),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).startOwn,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_language_filter.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_search_provider.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_filter.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_client_extension.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/course_plans_repo.dart';
|
||||
import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_response.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/login/pages/add_course_page.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class NewCoursePage extends StatefulWidget {
|
||||
|
|
@ -30,15 +41,139 @@ class NewCoursePage extends StatefulWidget {
|
|||
State<NewCoursePage> createState() => NewCoursePageState();
|
||||
}
|
||||
|
||||
class NewCoursePageState extends State<NewCoursePage>
|
||||
with CourseSearchProvider {
|
||||
class NewCoursePageState extends State<NewCoursePage> {
|
||||
final ValueNotifier<Result<GetLocalizedCoursesResponse>?> _courses =
|
||||
ValueNotifier(null);
|
||||
|
||||
final ValueNotifier<LanguageModel?> _targetLanguageFilter =
|
||||
ValueNotifier(null);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final target = MatrixState.pangeaController.languageController.userL2;
|
||||
if (target != null) {
|
||||
setTargetLanguageFilter(target, reload: false);
|
||||
_targetLanguageFilter.value =
|
||||
MatrixState.pangeaController.languageController.userL2;
|
||||
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
CourseFilter get _filter {
|
||||
return CourseFilter(
|
||||
targetLanguage: _targetLanguageFilter.value,
|
||||
);
|
||||
}
|
||||
|
||||
void _setTargetLanguageFilter(LanguageModel? language) {
|
||||
if (_targetLanguageFilter.value?.langCodeShort == language?.langCodeShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
_targetLanguageFilter.value = language;
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
Future<void> _loadCourses() async {
|
||||
try {
|
||||
_courses.value = null;
|
||||
final resp = await CoursePlansRepo.searchByFilter(filter: _filter);
|
||||
_courses.value = Result.value(resp);
|
||||
if (resp.coursePlans.isEmpty) {
|
||||
ErrorHandler.logError(
|
||||
e: "No courses found",
|
||||
data: {
|
||||
'filter': _filter.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'filter': _filter.toJson(),
|
||||
},
|
||||
);
|
||||
_courses.value = Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSelect(CoursePlanModel course) async {
|
||||
final existingRoom =
|
||||
Matrix.of(context).client.getRoomByCourseId(course.uuid);
|
||||
|
||||
if (existingRoom == null) {
|
||||
context.go(
|
||||
widget.spaceId != null
|
||||
? '/rooms/spaces/${widget.spaceId}/addcourse/${course.uuid}'
|
||||
: '/${widget.route}/course/own/${course.uuid}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final action = await showAdaptiveDialog<int>(
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: Center(
|
||||
child: Text(
|
||||
course.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: Text(
|
||||
L10n.of(context).alreadyInCourseWithID,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(0),
|
||||
bigButtons: true,
|
||||
child: Text(L10n.of(context).createCourse),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(1),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).goToExistingCourse,
|
||||
),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (action == 0) {
|
||||
context.go(
|
||||
widget.spaceId != null
|
||||
? '/rooms/spaces/${widget.spaceId}/addcourse/${course.uuid}'
|
||||
: '/${widget.route}/course/own/${course.uuid}',
|
||||
);
|
||||
} else if (action == 1) {
|
||||
if (existingRoom.isSpace) {
|
||||
context.go('/rooms/spaces/${existingRoom.id}');
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
e: "Existing course room is not a space",
|
||||
data: {
|
||||
'roomId': existingRoom.id,
|
||||
'courseId': course.uuid,
|
||||
},
|
||||
);
|
||||
context.go('/rooms/${existingRoom.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +181,6 @@ class NewCoursePageState extends State<NewCoursePage>
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final spaceId = widget.spaceId;
|
||||
final courseEntries = courses.entries.toList();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
|
|
@ -88,9 +222,14 @@ class NewCoursePageState extends State<NewCoursePage>
|
|||
runSpacing: 8.0,
|
||||
alignment: WrapAlignment.start,
|
||||
children: [
|
||||
CourseLanguageFilter(
|
||||
value: targetLanguageFilter,
|
||||
onChanged: setTargetLanguageFilter,
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _targetLanguageFilter,
|
||||
builder: (context, value, __) {
|
||||
return CourseLanguageFilter(
|
||||
value: _targetLanguageFilter.value,
|
||||
onChanged: _setTargetLanguageFilter,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -99,8 +238,14 @@ class NewCoursePageState extends State<NewCoursePage>
|
|||
),
|
||||
const SizedBox(height: 20.0),
|
||||
],
|
||||
loading || error != null || courses.isEmpty
|
||||
? Center(
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _courses,
|
||||
builder: (context, value, __) {
|
||||
final loading = value == null;
|
||||
if (loading ||
|
||||
value.isError ||
|
||||
value.result!.coursePlans.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: loading
|
||||
|
|
@ -142,84 +287,88 @@ class NewCoursePageState extends State<NewCoursePage>
|
|||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 10.0),
|
||||
itemCount: courseEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final course = courseEntries[index].value;
|
||||
final courseId = courseEntries[index].key;
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => context.go(
|
||||
spaceId != null
|
||||
? '/rooms/spaces/$spaceId/addcourse/$courseId'
|
||||
: '/${widget.route}/course/own/$courseId',
|
||||
);
|
||||
}
|
||||
|
||||
final courseEntries =
|
||||
value.result!.coursePlans.entries.toList();
|
||||
|
||||
return Expanded(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 10.0),
|
||||
itemCount: courseEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final course = courseEntries[index].value;
|
||||
final courseId = courseEntries[index].key;
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => _onSelect(course),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ImageByUrl(
|
||||
imageUrl: course.imageUrl,
|
||||
width: 58.0,
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
replacement: Container(
|
||||
height: 58.0,
|
||||
width: 58.0,
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
replacement: Container(
|
||||
height: 58.0,
|
||||
width: 58.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.0),
|
||||
color: theme.colorScheme
|
||||
.surfaceContainer,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
10.0,
|
||||
),
|
||||
color: theme
|
||||
.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
course.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
course.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
CourseInfoChips(
|
||||
courseId,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CourseInfoChips(
|
||||
courseId,
|
||||
iconSize: 12.0,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue