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:
ggurdin 2025-10-22 10:20:52 -04:00 committed by GitHub
parent 288aee68eb
commit 2efd61bccd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 261 additions and 150 deletions

View file

@ -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 youve earned and how!"
"levelInfoTooltip": "Here you can see all the points youve 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"
}

View file

@ -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);
}
}
}

View file

@ -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,
);
}

View file

@ -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,
),
),
],
),
),

View file

@ -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,
),
],
),
),
);
},
),
),
);
},
),
);
},
),
],
),
),