From 3ed4add04e7c894e43f27e34380b9531d5f3dbf2 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:09:55 -0400 Subject: [PATCH] feat: new onboarding flow (#4112) * feat: new onboarding flow * go to course details page on click course template in setup page * update route redirects * style tweaks --- lib/config/routes.dart | 134 +++++++-- lib/l10n/intl_en.arb | 28 ++ lib/pages/login/login.dart | 10 +- .../course_info_chip_widget.dart | 12 +- .../course_plan_filter_widget.dart | 17 +- .../course_plan_tile_widget.dart | 12 +- .../course_search_provider.dart | 63 +++++ .../course_creation/new_course_page.dart | 184 +++++++++---- .../course_creation/new_course_view.dart | 153 ----------- lib/pangea/guard/p_vguard.dart | 161 +++-------- .../login/pages/language_selection_page.dart | 145 ++++++++++ .../login/pages/login_options_view.dart | 102 +++++++ .../login/pages/login_or_signup_view.dart | 121 ++++++--- lib/pangea/login/pages/new_trip_page.dart | 204 ++++++++++++++ lib/pangea/login/pages/pangea_login_view.dart | 231 ++++++++-------- lib/pangea/login/pages/plan_trip_page.dart | 256 ++++++++++++++++++ lib/pangea/login/pages/private_trip_page.dart | 117 ++++++++ lib/pangea/login/pages/public_trip_page.dart | 164 +++++++++++ lib/pangea/login/pages/signup.dart | 12 +- lib/pangea/login/pages/signup_view.dart | 139 ++++++---- .../login/pages/signup_with_email_view.dart | 129 +++++---- lib/pangea/login/widgets/p_sso_button.dart | 58 ++-- lib/pangea/toolbar/widgets/icon_rain.dart | 16 +- .../user/controllers/user_controller.dart | 2 +- lib/pangea/user/utils/p_logout.dart | 22 +- lib/widgets/matrix.dart | 13 +- 26 files changed, 1831 insertions(+), 674 deletions(-) create mode 100644 lib/pangea/course_creation/course_search_provider.dart delete mode 100644 lib/pangea/course_creation/new_course_view.dart create mode 100644 lib/pangea/login/pages/language_selection_page.dart create mode 100644 lib/pangea/login/pages/login_options_view.dart create mode 100644 lib/pangea/login/pages/new_trip_page.dart create mode 100644 lib/pangea/login/pages/plan_trip_page.dart create mode 100644 lib/pangea/login/pages/private_trip_page.dart create mode 100644 lib/pangea/login/pages/public_trip_page.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 180457ac3..2391aad11 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -41,10 +41,13 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/find_your_people/find_your_people.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; +import 'package:fluffychat/pangea/login/pages/language_selection_page.dart'; import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart'; +import 'package:fluffychat/pangea/login/pages/new_trip_page.dart'; +import 'package:fluffychat/pangea/login/pages/plan_trip_page.dart'; +import 'package:fluffychat/pangea/login/pages/private_trip_page.dart'; +import 'package:fluffychat/pangea/login/pages/public_trip_page.dart'; import 'package:fluffychat/pangea/login/pages/signup.dart'; -import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart'; -import 'package:fluffychat/pangea/login/pages/user_settings.dart'; import 'package:fluffychat/pangea/onboarding/onboarding.dart'; import 'package:fluffychat/pangea/space_analytics/space_analytics.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -116,27 +119,56 @@ abstract class AppRoutes { // Pangea# ), redirect: loggedInRedirect, - ), - // #Pangea - GoRoute( - path: 'signup', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SignupPage(), - ), - redirect: loggedInRedirect, + // #Pangea routes: [ GoRoute( path: 'email', pageBuilder: (context, state) => defaultPageBuilder( context, state, - const SignupPage(withEmail: true), + const Login(withEmail: true), ), redirect: loggedInRedirect, ), ], + // Pangea# + ), + // #Pangea + GoRoute( + path: 'languages', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const LanguageSelectionPage(), + ), + redirect: loggedInRedirect, + routes: [ + GoRoute( + path: ':langcode', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + SignupPage( + langCode: state.pathParameters['langcode']!, + ), + ), + redirect: loggedInRedirect, + routes: [ + GoRoute( + path: 'email', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + SignupPage( + withEmail: true, + langCode: state.pathParameters['langcode']!, + ), + ), + redirect: loggedInRedirect, + ), + ], + ), + ], ), // Pangea# ], @@ -177,24 +209,80 @@ abstract class AppRoutes { ), ), GoRoute( - path: '/user_age', + path: '/course', pageBuilder: (context, state) => defaultPageBuilder( context, state, - const UserSettingsPage(), + const LanguageSelectionPage(), ), redirect: loggedOutRedirect, routes: [ GoRoute( - path: 'join_space', - pageBuilder: (context, state) { - return defaultPageBuilder( - context, - state, - const SpaceCodeOnboarding(), - ); - }, + path: ':langcode', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + PlanTripPage( + langCode: state.pathParameters['langcode']!, + ), + ), redirect: loggedOutRedirect, + routes: [ + GoRoute( + path: 'private', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + PrivateTripPage( + langCode: state.pathParameters['langcode']!, + ), + ); + }, + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'public', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + PublicTripPage( + langCode: state.pathParameters['langcode']!, + ), + ); + }, + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'own', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + NewTripPage( + langCode: state.pathParameters['langcode']!, + ), + ); + }, + redirect: loggedOutRedirect, + routes: [ + GoRoute( + path: ':courseid', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + SelectedCourse( + state.pathParameters['courseid']!, + ), + ); + }, + redirect: loggedOutRedirect, + ), + ], + ), + ], ), ], ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ce74bf696..b9df1a2ba 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5257,6 +5257,34 @@ "readingAnalyticsDesc": "Click practice on each message for reading activities.", "speakingAnalyticsDesc": "Record voice messages for speaking practice.", "audioAnalyticsDesc": "Click practice on each message for listening activities.", + "loginToAccount": "Login to my account", + "appDescription": "Learn a language\nwhile texting your friends.", + "languages": "Languages", + "chooseLanguage": "Choose a language.", + "letsGo": "Let's go", + "planTrip": "Plan your trip", + "howAreYouTraveling": "How are you traveling?", + "unlockPrivateTrip": "Unlock a private trip", + "joinPublicTrip": "Join a public trip", + "startOwnTrip": "Start my own", + "tripPlanDesc": "Trips are courses. Each has 8-10 sequenced topics with a range of task-based language learning activities.", + "unlockPrivateTripTitle": "Unlock private trip", + "browsePublicTrips": "Browse public trips", + "startOwnTripTitle": "Start my own trip", + "courseCode": "What’s the secret password?", + "courseCodeHint": "Trip code or link", + "unlockMyTrip": "Unlock my trip", + "anyLevel": "Any Level", + "signupOption": "How do you want to sign up?", + "withApple": "With Apple", + "withGoogle": "With Google", + "withEmail": "With Email", + "createAccount": "Create account", + "noCoursesFound": "No courses found", + "loginWithEmail": "Login with email", + "usernameOrEmail": "Username or email", + "email": "Email", + "forgotPassword": "Forgot password?", "writingAnalyticsDesc": "Send messages to practice writing.", "endActivity": "End activity" } diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 86ab02fc8..eadf2cb21 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -4,6 +4,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/login/pages/login_options_view.dart'; import 'package:fluffychat/pangea/login/pages/pangea_login_view.dart'; import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart'; import 'package:fluffychat/pangea/user/utils/p_login.dart'; @@ -17,8 +18,12 @@ class Login extends StatefulWidget { // #Pangea // final Client client; // const Login({required this.client, super.key}); + final bool withEmail; - const Login({super.key}); + const Login({ + super.key, + this.withEmail = false, + }); // Pangea# @override @@ -329,7 +334,8 @@ class LoginController extends State { @override // #Pangea // Widget build(BuildContext context) => LoginView(this); - Widget build(BuildContext context) => PangeaLoginView(this); + Widget build(BuildContext context) => + widget.withEmail ? PasswordLoginView(this) : LoginOptionsView(this); // Pangea# } diff --git a/lib/pangea/course_creation/course_info_chip_widget.dart b/lib/pangea/course_creation/course_info_chip_widget.dart index 57eb1b78d..0c477de39 100644 --- a/lib/pangea/course_creation/course_info_chip_widget.dart +++ b/lib/pangea/course_creation/course_info_chip_widget.dart @@ -8,8 +8,8 @@ class CourseInfoChip extends StatelessWidget { final IconData icon; final String text; - final double fontSize; - final double iconSize; + final double? fontSize; + final double? iconSize; final EdgeInsets? padding; const CourseInfoChip({ @@ -47,15 +47,15 @@ class CourseInfoChip extends StatelessWidget { class CourseInfoChips extends StatelessWidget { final CoursePlanModel course; - final double fontSize; - final double iconSize; + final double? fontSize; + final double? iconSize; final EdgeInsets? padding; const CourseInfoChips( this.course, { super.key, - required this.fontSize, - required this.iconSize, + this.fontSize, + this.iconSize, this.padding, }); diff --git a/lib/pangea/course_creation/course_plan_filter_widget.dart b/lib/pangea/course_creation/course_plan_filter_widget.dart index cad9311d8..e63d5bbf1 100644 --- a/lib/pangea/course_creation/course_plan_filter_widget.dart +++ b/lib/pangea/course_creation/course_plan_filter_widget.dart @@ -15,9 +15,6 @@ class CoursePlanFilter extends StatefulWidget { final bool enableSearch; - final double fontSize; - final double iconSize; - const CoursePlanFilter({ super.key, required this.value, @@ -25,8 +22,6 @@ class CoursePlanFilter extends StatefulWidget { required this.onChanged, required this.defaultName, required this.displayname, - required this.fontSize, - required this.iconSize, this.enableSearch = false, this.shortName, }); @@ -51,7 +46,7 @@ class CoursePlanFilterState extends State> { child: DropdownButton2( customButton: Container( decoration: BoxDecoration( - color: theme.colorScheme.primary, + border: Border.all(color: theme.colorScheme.onSurface), borderRadius: BorderRadius.circular(12.0), ), padding: const EdgeInsets.symmetric( @@ -66,15 +61,11 @@ class CoursePlanFilterState extends State> { widget.value != null ? widget.displayname(widget.value as T) : widget.defaultName, - style: TextStyle( - color: theme.colorScheme.onPrimary, - fontSize: widget.fontSize, - ), + style: theme.textTheme.labelMedium, ), - Icon( + const Icon( Icons.arrow_drop_down, - color: theme.colorScheme.onPrimary, - size: widget.iconSize, + size: 12.0, ), ], ), diff --git a/lib/pangea/course_creation/course_plan_tile_widget.dart b/lib/pangea/course_creation/course_plan_tile_widget.dart index 516df058d..d9130bc49 100644 --- a/lib/pangea/course_creation/course_plan_tile_widget.dart +++ b/lib/pangea/course_creation/course_plan_tile_widget.dart @@ -12,17 +12,17 @@ class CoursePlanTile extends StatelessWidget { final CoursePlanModel course; final VoidCallback onTap; - final double titleFontSize; - final double chipFontSize; - final double chipIconSize; + final double? titleFontSize; + final double? chipFontSize; + final double? chipIconSize; const CoursePlanTile({ super.key, required this.course, required this.onTap, - required this.titleFontSize, - required this.chipFontSize, - required this.chipIconSize, + this.titleFontSize, + this.chipFontSize, + this.chipIconSize, }); @override diff --git a/lib/pangea/course_creation/course_search_provider.dart b/lib/pangea/course_creation/course_search_provider.dart new file mode 100644 index 000000000..ae30743f8 --- /dev/null +++ b/lib/pangea/course_creation/course_search_provider.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + +mixin CourseSearchProvider on State { + bool loading = true; + Object? error; + + List courses = []; + + LanguageLevelTypeEnum? languageLevelFilter; + LanguageModel? instructionLanguageFilter; + LanguageModel? targetLanguageFilter; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _loadCourses(), + ); + } + + CourseFilter get _filter { + return CourseFilter( + targetLanguage: targetLanguageFilter, + languageOfInstructions: instructionLanguageFilter, + cefrLevel: languageLevelFilter, + ); + } + + void setLanguageLevelFilter(LanguageLevelTypeEnum? level) { + languageLevelFilter = level; + _loadCourses(); + } + + void setInstructionLanguageFilter(LanguageModel? language) { + instructionLanguageFilter = language; + _loadCourses(); + } + + void setTargetLanguageFilter(LanguageModel? language) { + targetLanguageFilter = language; + _loadCourses(); + } + + Future _loadCourses() async { + try { + setState(() { + loading = true; + error = null; + }); + courses = await CoursePlansRepo.search(filter: _filter); + } catch (e, s) { + debugPrint("Failed to load courses: $e\n$s"); + error = e; + } finally { + if (mounted) setState(() => loading = false); + } + } +} diff --git a/lib/pangea/course_creation/new_course_page.dart b/lib/pangea/course_creation/new_course_page.dart index b8348ed76..50e198235 100644 --- a/lib/pangea/course_creation/new_course_page.dart +++ b/lib/pangea/course_creation/new_course_page.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/course_creation/new_course_view.dart'; -import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; -import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.dart'; +import 'package:fluffychat/pangea/course_creation/course_plan_tile_widget.dart'; +import 'package:fluffychat/pangea/course_creation/course_search_provider.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class NewCourse extends StatefulWidget { final String? spaceId; @@ -17,56 +23,132 @@ class NewCourse extends StatefulWidget { State createState() => NewCourseController(); } -class NewCourseController extends State { - bool loading = true; - Object? error; - - List courses = []; - - LanguageLevelTypeEnum? languageLevelFilter; - LanguageModel? instructionLanguageFilter; - LanguageModel? targetLanguageFilter; - +class NewCourseController extends State with CourseSearchProvider { @override - void initState() { - super.initState(); - _loadCourses(); - } + Widget build(BuildContext context) { + const double titleFontSize = 16.0; + const double descFontSize = 12.0; - CourseFilter get _filter { - return CourseFilter( - targetLanguage: targetLanguageFilter, - languageOfInstructions: instructionLanguageFilter, - cefrLevel: languageLevelFilter, + const double iconSize = 12.0; + final spaceId = widget.spaceId; + + return Scaffold( + appBar: AppBar( + title: Text( + spaceId != null + ? L10n.of(context).addCoursePlan + : L10n.of(context).newCourse, + ), + ), + body: Padding( + padding: const EdgeInsets.all(12.0), + child: MaxWidthBody( + showBorder: false, + withScrolling: false, + maxWidth: 500.0, + child: Column( + spacing: 12.0, + children: [ + Text( + L10n.of(context).newCourseSubtitle, + style: const TextStyle( + fontSize: titleFontSize, + fontStyle: FontStyle.italic, + ), + ), + Padding( + padding: const EdgeInsetsGeometry.symmetric( + vertical: 4.0, + ), + child: Row( + children: [ + Expanded( + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: [ + CoursePlanFilter( + value: instructionLanguageFilter, + onChanged: setInstructionLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.baseOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: + L10n.of(context).languageOfInstructionsLabel, + shortName: L10n.of(context).instructionsLanguage, + ), + CoursePlanFilter( + value: targetLanguageFilter, + onChanged: setTargetLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.targetOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: L10n.of(context).targetLanguageLabel, + ), + CoursePlanFilter( + value: languageLevelFilter, + onChanged: setLanguageLevelFilter, + items: LanguageLevelTypeEnum.values, + displayname: (v) => v.string, + defaultName: L10n.of(context).cefrLevelLabel, + ), + ], + ), + ), + ], + ), + ), + Builder( + builder: (context) { + if (error != null) { + return Center( + child: ErrorIndicator( + message: L10n.of(context).failedToLoadCourses, + ), + ); + } + + if (loading) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + + return Expanded( + child: ListView.builder( + itemCount: courses.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsetsGeometry.fromLTRB( + 4.0, + 4.0, + 4.0, + 16.0, + ), + child: CoursePlanTile( + course: courses[index], + onTap: () => context.go( + spaceId != null + ? "/rooms/spaces/$spaceId/addcourse/${courses[index].uuid}" + : "/rooms/communities/newcourse/${courses[index].uuid}", + ), + titleFontSize: titleFontSize, + chipFontSize: descFontSize, + chipIconSize: iconSize, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), ); } - - void setLanguageLevelFilter(LanguageLevelTypeEnum? level) { - languageLevelFilter = level; - _loadCourses(); - } - - void setInstructionLanguageFilter(LanguageModel? language) { - instructionLanguageFilter = language; - _loadCourses(); - } - - void setTargetLanguageFilter(LanguageModel? language) { - targetLanguageFilter = language; - _loadCourses(); - } - - Future _loadCourses() async { - try { - setState(() => loading = true); - courses = await CoursePlansRepo.search(filter: _filter); - } catch (e) { - error = e; - } finally { - setState(() => loading = false); - } - } - - @override - Widget build(BuildContext context) => NewCourseView(this); } diff --git a/lib/pangea/course_creation/new_course_view.dart b/lib/pangea/course_creation/new_course_view.dart deleted file mode 100644 index 0ee9a1943..000000000 --- a/lib/pangea/course_creation/new_course_view.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.dart'; -import 'package:fluffychat/pangea/course_creation/course_plan_tile_widget.dart'; -import 'package:fluffychat/pangea/course_creation/new_course_page.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class NewCourseView extends StatelessWidget { - final NewCourseController controller; - - const NewCourseView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - const double titleFontSize = 16.0; - const double descFontSize = 12.0; - - const double iconSize = 12.0; - final spaceId = controller.widget.spaceId; - - return Scaffold( - appBar: AppBar( - title: Text( - spaceId != null - ? L10n.of(context).addCoursePlan - : L10n.of(context).newCourse, - ), - ), - body: Padding( - padding: const EdgeInsets.all(12.0), - child: MaxWidthBody( - showBorder: false, - withScrolling: false, - maxWidth: 500.0, - child: Column( - spacing: 12.0, - children: [ - Text( - L10n.of(context).newCourseSubtitle, - style: const TextStyle( - fontSize: titleFontSize, - fontStyle: FontStyle.italic, - ), - ), - Padding( - padding: const EdgeInsetsGeometry.symmetric( - vertical: 4.0, - ), - child: Row( - children: [ - Expanded( - child: Wrap( - spacing: 4.0, - runSpacing: 4.0, - alignment: WrapAlignment.start, - children: [ - CoursePlanFilter( - value: controller.languageLevelFilter, - onChanged: controller.setLanguageLevelFilter, - items: LanguageLevelTypeEnum.values, - displayname: (v) => v.string, - fontSize: descFontSize, - iconSize: iconSize, - defaultName: L10n.of(context).cefrLevelLabel, - ), - CoursePlanFilter( - value: controller.instructionLanguageFilter, - onChanged: controller.setInstructionLanguageFilter, - items: MatrixState - .pangeaController.pLanguageStore.baseOptions, - displayname: (v) => - v.getDisplayName(context) ?? v.displayName, - enableSearch: true, - fontSize: descFontSize, - iconSize: iconSize, - defaultName: - L10n.of(context).languageOfInstructionsLabel, - shortName: L10n.of(context).instructionsLanguage, - ), - CoursePlanFilter( - value: controller.targetLanguageFilter, - onChanged: controller.setTargetLanguageFilter, - items: MatrixState - .pangeaController.pLanguageStore.targetOptions, - displayname: (v) => - v.getDisplayName(context) ?? v.displayName, - enableSearch: true, - fontSize: descFontSize, - iconSize: iconSize, - defaultName: L10n.of(context).targetLanguageLabel, - ), - ], - ), - ), - ], - ), - ), - Builder( - builder: (context) { - if (controller.error != null) { - return Center( - child: ErrorIndicator( - message: L10n.of(context).failedToLoadCourses, - ), - ); - } - - if (controller.loading) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - - return Expanded( - child: ListView.builder( - itemCount: controller.courses.length, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsetsGeometry.fromLTRB( - 4.0, - 4.0, - 4.0, - 16.0, - ), - child: CoursePlanTile( - course: controller.courses[index], - onTap: () => context.go( - spaceId != null - ? "/rooms/spaces/$spaceId/addcourse/${controller.courses[index].uuid}" - : "/rooms/communities/newcourse/${controller.courses[index].uuid}", - ), - titleFontSize: titleFontSize, - chipFontSize: descFontSize, - chipIconSize: iconSize, - ), - ), - ), - ); - }, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pangea/guard/p_vguard.dart b/lib/pangea/guard/p_vguard.dart index 72f7aa6e4..65bcffd9a 100644 --- a/lib/pangea/guard/p_vguard.dart +++ b/lib/pangea/guard/p_vguard.dart @@ -11,146 +11,55 @@ class PAuthGaurd { static bool isPublicLeaving = false; static PangeaController? pController; + /// Redirect for /home routes static FutureOr loggedInRedirect( BuildContext context, GoRouterState state, ) async { - if (pController != null) { - if (Matrix.of(context) - .widget - .clients - .any((client) => client.isLogged())) { - final bool dobIsSet = - await pController!.userController.isUserDataAvailableAndL2Set; - return dobIsSet ? '/rooms' : '/user_age'; - } - return null; - } else { - debugPrint("controller is null in pguard check"); - Matrix.of(context).client.isLogged() ? '/rooms' : null; + if (pController == null) { + return Matrix.of(context).client.isLogged() ? '/rooms' : null; } - return null; + + final isLogged = + Matrix.of(context).widget.clients.any((client) => client.isLogged()); + if (!isLogged) return null; + + return _onboardingRedirect(context, state); } + /// Redirect for onboarding and /rooms routes static FutureOr loggedOutRedirect( BuildContext context, GoRouterState state, ) async { - if (pController != null) { - if (!Matrix.of(context) - .widget - .clients - .any((client) => client.isLogged())) { - return '/home'; - } - final bool dobIsSet = - await pController!.userController.isUserDataAvailableAndL2Set; - return dobIsSet ? null : '/user_age'; - } else { - debugPrint("controller is null in pguard check"); + if (pController == null) { return Matrix.of(context).client.isLogged() ? null : '/home'; } + + final isLogged = + Matrix.of(context).widget.clients.any((client) => client.isLogged()); + if (!isLogged) { + return '/home'; + } + + return _onboardingRedirect(context, state); } - // static const defaultRoute = '/home'; + static Future _onboardingRedirect( + BuildContext context, + GoRouterState state, + ) async { + // If user hasn't set their L2, + // and their URL doesn’t include ‘course,’ redirect + final bool hasSetL2 = await pController!.userController.isUserL2Set; + final bool shouldRedirect = + !hasSetL2 && !(state.fullPath?.contains('course') ?? false); - // static Future onPublicEnter() async { - // final bool setDob = - // await pController!.userController.isUserDataAvailableAndDateOfBirthSet; - // if (_isLogged != null && _isLogged! && setDob) { - // vRedirector.to('/rooms'); - // } - // } - - // static Future onPublicUpdate(VRedirector vRedirector) async { - // final bool setDob = - // await pController!.userController.isUserDataAvailableAndDateOfBirthSet; - // if (_isLogged != null && _isLogged! && setDob) { - // vRedirector.to('/rooms'); - // } - // bool oldHaveParms = false; - - // final bool haveData = vRedirector.previousVRouterData != null; - // if (haveData) { - // final bool isPublicRoute = - // vRedirector.newVRouterData!.url!.startsWith(defaultRoute); - // if (!isPublicRoute) { - // return; - // } - // oldHaveParms = - // vRedirector.previousVRouterData!.queryParameters.isNotEmpty; - // if (oldHaveParms) { - // if (vRedirector.newVRouterData!.queryParameters.isEmpty) { - // vRedirector.to( - // vRedirector.toUrl!, - // queryParameters: vRedirector.previousVRouterData!.queryParameters, - // ); - // } - // } - // } - - // return; - // } - - // static Future onPublicLeave( - // VRedirector vRedirector, - // Function(Map onLeave) callback, - // ) async { - // final bool haveData = vRedirector.previousVRouterData != null; - - // if (haveData) { - // try { - // if (vRedirector.previousVRouterData!.queryParameters['redirect'] == - // 'true') { - // if (!isPublicLeaving) { - // isPublicLeaving = true; - // vRedirector.to( - // vRedirector.previousVRouterData!.queryParameters['redirectPath']!, - // ); - // } - // } - // } catch (e, s) { - // ErrorHandler.logError(e: e, s: s); - // } - // } - // return; - // } - - // static Future onPrivateUpdate(VRedirector vRedirector) async { - // if (_isLogged == null) { - // return; - // } - // final Map redirectParm = {}; - // final bool haveData = vRedirector.newVRouterData != null; - // if (haveData) { - // if (vRedirector.newVRouterData!.queryParameters.isNotEmpty) { - // redirectParm['redirect'] = 'true'; - // redirectParm['redirectPath'] = vRedirector.newVRouterData!.url!; - // } - // } - // if (!_isLogged!) { - // debugPrint("onPrivateUpdate with user not logged in"); - // ErrorHandler.logError( - // e: Exception("onPrivateUpdate with user not logged in"), - // s: StackTrace.current, - // ); - // // vRedirector.to(defaultRoute, queryParameters: redirectParm); - // } else { - // if (pController != null) { - // if (!await pController! - // .userController.isUserDataAvailableAndDateOfBirthSet) { - // debugPrint("reroute to user_age"); - // vRedirector.to( - // '/home/connect/user_age', - // queryParameters: redirectParm, - // ); - // } - // } else { - // debugPrint("controller is null in pguard check"); - // } - // } - - // isPublicLeaving = false; - // return; - // } + final langCode = state.pathParameters['langcode']; + return shouldRedirect + ? langCode != null + ? '/course/$langCode' + : '/course' + : null; + } } diff --git a/lib/pangea/login/pages/language_selection_page.dart b/lib/pangea/login/pages/language_selection_page.dart new file mode 100644 index 000000000..339cc65c1 --- /dev/null +++ b/lib/pangea/login/pages/language_selection_page.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LanguageSelectionPage extends StatefulWidget { + const LanguageSelectionPage({super.key}); + + @override + State createState() => LanguageSelectionPageState(); +} + +class LanguageSelectionPageState extends State { + LanguageModel? _selectedLanguage; + + @override + void initState() { + super.initState(); + } + + void _setSelectedLanguage(LanguageModel? l) { + setState(() => _selectedLanguage = l); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final languages = MatrixState.pangeaController.pLanguageStore.targetOptions; + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context).languages), + ), + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 450, + ), + child: Column( + spacing: 24.0, + children: [ + const SizedBox(height: 50.0), + Expanded( + child: Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 60.0, + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.center, + children: languages + .map( + (l) => FilterChip( + selected: _selectedLanguage == l, + backgroundColor: _selectedLanguage == l + ? theme.colorScheme.primary + : theme.colorScheme.surface, + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + label: Text( + l.getDisplayName(context) ?? + l.displayName, + style: theme.textTheme.bodyMedium, + ), + onSelected: (selected) { + _setSelectedLanguage( + selected ? l : null, + ); + }, + ), + ) + .toList(), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: IgnorePointer( + child: Container( + height: 100, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + theme.colorScheme.surface, + theme.colorScheme.surface.withAlpha(0), + ], + ), + ), + ), + ), + ), + ], + ), + ), + Text( + L10n.of(context).chooseLanguage, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ElevatedButton( + onPressed: _selectedLanguage != null + ? () => context.go( + Matrix.of(context).client.isLogged() + ? "/course/${_selectedLanguage!.langCode}" + : "/home/languages/${_selectedLanguage!.langCode}", + ) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).letsGo), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/login_options_view.dart b/lib/pangea/login/pages/login_options_view.dart new file mode 100644 index 000000000..57eeec9da --- /dev/null +++ b/lib/pangea/login/pages/login_options_view.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/login/login.dart'; +import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; +import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart'; + +class LoginOptionsView extends StatelessWidget { + final LoginController controller; + + const LoginOptionsView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context).loginToAccount, + ), + ), + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 600, + ), + child: Column( + spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PangeaSsoButton( + provider: SSOProvider.apple, + title: "Apple", + loading: controller.loadingAppleSSO, + setLoading: controller.setLoadingSSO, + ), + PangeaSsoButton( + provider: SSOProvider.google, + title: "Google", + loading: controller.loadingGoogleSSO, + setLoading: controller.setLoadingSSO, + ), + ElevatedButton( + onPressed: () => context.go('/home/login/email'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PangeaLogoSvg( + width: 20, + forceColor: Theme.of(context).colorScheme.onSurface, + ), + Text(L10n.of(context).email), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + text: L10n.of(context).byUsingPangeaChat, + children: [ + TextSpan( + text: L10n.of(context).termsAndConditions, + style: TextStyle( + decoration: TextDecoration.underline, + color: theme.colorScheme.primary, + ), + ), + TextSpan( + text: + L10n.of(context).andCertifyIAmAtLeast13YearsOfAge, + ), + ], + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/login_or_signup_view.dart b/lib/pangea/login/pages/login_or_signup_view.dart index 1b8dcd0d5..8d20a710e 100644 --- a/lib/pangea/login/pages/login_or_signup_view.dart +++ b/lib/pangea/login/pages/login_or_signup_view.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; +import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; import 'package:fluffychat/pangea/login/widgets/app_config_dialog.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; -import 'package:fluffychat/widgets/matrix.dart'; class LoginOrSignupView extends StatefulWidget { const LoginOrSignupView({super.key}); @@ -18,17 +16,12 @@ class LoginOrSignupView extends StatefulWidget { } class LoginOrSignupViewState extends State { - Client? client; List _overrides = []; @override void initState() { super.initState(); _loadOverrides(); - - Matrix.of(context).getLoginClient().then((c) { - if (mounted) setState(() => client = c); - }); } Future _loadOverrides() async { @@ -52,30 +45,94 @@ class LoginOrSignupViewState extends State { @override Widget build(BuildContext context) { - return PangeaLoginScaffold( - actions: Environment.isStagingEnvironment && _overrides.isNotEmpty - ? [ - IconButton( - icon: const Icon(Icons.settings_outlined), - onPressed: _setEnvironment, - ), - ] - : null, - children: [ - FullWidthButton( - title: L10n.of(context).createAnAccount, - onPressed: () => context.go('/home/signup'), + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + actions: Environment.isStagingEnvironment && _overrides.isNotEmpty + ? [ + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: _setEnvironment, + ), + ] + : null, + ), + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + ), + child: Column( + spacing: 50.0, + mainAxisSize: MainAxisSize.min, + children: [ + Column( + spacing: 12.0, + children: [ + PangeaLogoSvg( + width: 50.0, + forceColor: theme.colorScheme.onSurface, + ), + Text( + AppConfig.applicationName, + style: theme.textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + Text( + L10n.of(context).appDescription, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () => context.go('/home/languages'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).start), + ], + ), + ), + ElevatedButton( + onPressed: () => context.go('/home/login'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).loginToAccount), + ], + ), + ), + ], + ), + ], + ), + ), ), - FullWidthButton( - title: L10n.of(context).signIn, - onPressed: client != null - ? () => context.go( - '/home/login', - extra: Matrix.of(context).client, - ) - : null, - ), - ], + ), ); } } diff --git a/lib/pangea/login/pages/new_trip_page.dart b/lib/pangea/login/pages/new_trip_page.dart new file mode 100644 index 000000000..0ac51d164 --- /dev/null +++ b/lib/pangea/login/pages/new_trip_page.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.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_plan_filter_widget.dart'; +import 'package:fluffychat/pangea/course_creation/course_search_provider.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class NewTripPage extends StatefulWidget { + final String langCode; + const NewTripPage({ + super.key, + required this.langCode, + }); + + @override + State createState() => NewTripPageState(); +} + +class NewTripPageState extends State with CourseSearchProvider { + @override + void initState() { + super.initState(); + + final target = PLanguageStore.byLangCode(widget.langCode); + if (target != null) { + setTargetLanguageFilter(target); + } + + final base = MatrixState.pangeaController.languageController.systemLanguage; + if (base != null) { + setInstructionLanguageFilter(base); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Row( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).startOwnTripTitle), + ], + ), + ), + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 450, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: [ + CoursePlanFilter( + value: instructionLanguageFilter, + onChanged: setInstructionLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.baseOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: + L10n.of(context).languageOfInstructionsLabel, + shortName: L10n.of(context).instructionsLanguage, + ), + CoursePlanFilter( + value: targetLanguageFilter, + onChanged: setTargetLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.targetOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: L10n.of(context).targetLanguageLabel, + ), + CoursePlanFilter( + value: languageLevelFilter, + onChanged: setLanguageLevelFilter, + items: LanguageLevelTypeEnum.values, + displayname: (v) => v.string, + defaultName: L10n.of(context).cefrLevelLabel, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20.0), + loading || error != null || courses.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: error != null + ? Center( + child: ErrorIndicator( + message: + L10n.of(context).failedToLoadCourses, + ), + ) + : loading + ? const CircularProgressIndicator.adaptive() + : Text(L10n.of(context).noCoursesFound), + ), + ) + : Expanded( + child: ListView.builder( + itemCount: courses.length, + itemBuilder: (context, index) { + final course = courses[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: InkWell( + onTap: () => context.go( + '/course/${widget.langCode}/own/${course.uuid}', + ), + 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, + 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, + ), + ), + ), + Flexible( + child: Text( + course.title, + style: theme.textTheme.bodyLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + CourseInfoChips( + course, + iconSize: 12.0, + fontSize: 12.0, + ), + Text( + course.description, + style: theme.textTheme.bodyMedium, + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/pangea_login_view.dart b/lib/pangea/login/pages/pangea_login_view.dart index 26aec3bb2..1bfb6b8bf 100644 --- a/lib/pangea/login/pages/pangea_login_view.dart +++ b/lib/pangea/login/pages/pangea_login_view.dart @@ -1,149 +1,132 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/login/login.dart'; -import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; -import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; -import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart'; -class PangeaLoginView extends StatelessWidget { +class PasswordLoginView extends StatelessWidget { final LoginController controller; - const PangeaLoginView(this.controller, {super.key}); + const PasswordLoginView(this.controller, {super.key}); @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Form( key: controller.formKey, - child: PangeaLoginScaffold( - showAppName: FluffyThemes.isColumnMode(context), - children: [ - AutofillGroup( - child: Column( - children: [ - FullWidthTextField( - hintText: L10n.of(context).username, - autofillHints: const [AutofillHints.username], - autoFocus: true, - textInputAction: TextInputAction.next, - validator: (value) { - if (value == null || value.isEmpty) { - return L10n.of(context).pleaseEnterYourUsername; - } - return null; - }, - controller: controller.usernameController, - ), - FullWidthTextField( - hintText: L10n.of(context).password, - autofillHints: const [AutofillHints.password], - autoFocus: true, - obscureText: !controller.showPassword, - textInputAction: TextInputAction.go, - onSubmitted: (_) { - controller.enabledSignIn ? controller.login() : null; - }, - validator: (value) { - if (value == null || value.isEmpty) { - return L10n.of(context).pleaseEnterYourPassword; - } - return null; - }, - controller: controller.passwordController, - suffix: IconButton( - icon: Icon( - controller.showPassword - ? Icons.visibility_off - : Icons.visibility, - ), - onPressed: controller.toggleShowPassword, - ), - ), - ], - ), + child: Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context).loginWithEmail, ), - FullWidthButton( - title: L10n.of(context).signIn, - icon: PangeaLogoSvg( - width: 20, - forceColor: Theme.of(context).colorScheme.onPrimary, - ), - onPressed: controller.enabledSignIn ? controller.login : null, - loading: controller.loadingSignIn, - enabled: controller.enabledSignIn, - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: TextButton( - onPressed: controller.loadingSignIn || controller.client == null - ? () {} - : controller.passwordForgotten, - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4.0, - ), - minimumSize: const Size(0, 0), + ), + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 600, ), - child: Text(L10n.of(context).passwordForgotten), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const Expanded(child: Divider()), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text(L10n.of(context).or), - ), - const Expanded(child: Divider()), - ], - ), - ), - PangeaSsoButton( - provider: SSOProvider.google, - title: L10n.of(context).signInWithGoogle, - loading: controller.loadingGoogleSSO, - setLoading: controller.setLoadingSSO, - ), - PangeaSsoButton( - provider: SSOProvider.apple, - title: L10n.of(context).signInWithApple, - loading: controller.loadingAppleSSO, - setLoading: controller.setLoadingSSO, - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 8.0, - ), - child: RichText( - textAlign: TextAlign.justify, - text: TextSpan( - text: L10n.of(context).byUsingPangeaChat, + child: Column( + spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.end, children: [ - TextSpan( - text: L10n.of(context).termsAndConditions, - style: const TextStyle( - decoration: TextDecoration.underline, + AutofillGroup( + child: Column( + spacing: 16.0, + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context).usernameOrEmail, + ), + autofillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + validator: (value) { + if (value == null || value.isEmpty) { + return L10n.of(context).pleaseEnterYourUsername; + } + return null; + }, + controller: controller.usernameController, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + autofillHints: const [AutofillHints.password], + obscureText: !controller.showPassword, + textInputAction: TextInputAction.go, + onFieldSubmitted: (_) { + controller.enabledSignIn + ? controller.login() + : null; + }, + validator: (value) { + if (value == null || value.isEmpty) { + return L10n.of(context) + .pleaseEnterYourPassword; + } + return null; + }, + controller: controller.passwordController, + decoration: InputDecoration( + hintText: L10n.of(context).password, + suffixIcon: IconButton( + icon: Icon( + controller.showPassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: controller.toggleShowPassword, + ), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + TextButton( + onPressed: controller.loadingSignIn || + controller.client == null + ? () {} + : controller.passwordForgotten, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 4.0, + ), + minimumSize: const Size(0, 0), + ), + child: Text(L10n.of(context).forgotPassword), + ), + ], + ), + ], ), ), - TextSpan( - text: L10n.of(context).andCertifyIAmAtLeast13YearsOfAge, + ElevatedButton( + onPressed: + controller.enabledSignIn ? controller.login : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).login), + ], + ), ), ], - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - ), ), ), ), - ], + ), ), ); } diff --git a/lib/pangea/login/pages/plan_trip_page.dart b/lib/pangea/login/pages/plan_trip_page.dart new file mode 100644 index 000000000..ab1397a8a --- /dev/null +++ b/lib/pangea/login/pages/plan_trip_page.dart @@ -0,0 +1,256 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PlanTripPage extends StatefulWidget { + final String langCode; + const PlanTripPage({ + super.key, + required this.langCode, + }); + + @override + State createState() => PlanTripPageState(); +} + +class PlanTripPageState extends State { + bool _loadingProfile = true; + Object? _profileError; + + final List avatarPaths = const [ + "assets/pangea/Avatar_1.png", + "assets/pangea/Avatar_2.png", + "assets/pangea/Avatar_3.png", + "assets/pangea/Avatar_4.png", + "assets/pangea/Avatar_5.png", + ]; + + @override + void initState() { + super.initState(); + _createUserInPangea(); + } + + Future _setAvatar() async { + final client = Matrix.of(context).client; + try { + final random = Random(); + final selectedAvatarPath = + avatarPaths[random.nextInt(avatarPaths.length)]; + + final ByteData byteData = await rootBundle.load(selectedAvatarPath); + final Uint8List bytes = byteData.buffer.asUint8List(); + final file = MatrixFile( + bytes: bytes, + name: selectedAvatarPath, + ); + await client.setAvatar(file); + } catch (err, s) { + ErrorHandler.logError( + e: err, + s: s, + data: {}, + ); + } + } + + Future _createUserInPangea() async { + final l2Set = await MatrixState.pangeaController.userController.isUserL2Set; + if (l2Set) { + if (mounted) setState(() => _loadingProfile = false); + return; + } + + if (mounted) { + setState(() { + _loadingProfile = true; + _profileError = null; + }); + } + + try { + final updateFuture = [ + _setAvatar(), + MatrixState.pangeaController.userController.updateProfile( + (profile) { + final systemLang = MatrixState + .pangeaController.languageController.systemLanguage?.langCode; + + if (systemLang != null) { + profile.userSettings.sourceLanguage = systemLang; + } + + profile.userSettings.targetLanguage = widget.langCode; + profile.userSettings.createdAt = DateTime.now(); + return profile; + }, + waitForDataInSync: true, + ), + MatrixState.pangeaController.userController.updateAnalyticsProfile( + targetLanguage: PLanguageStore.byLangCode(widget.langCode), + baseLanguage: + MatrixState.pangeaController.languageController.systemLanguage, + level: 1, + ), + ]; + + await Future.wait(updateFuture).timeout( + const Duration(seconds: 30), + onTimeout: () { + throw TimeoutException(L10n.of(context).oopsSomethingWentWrong); + }, + ); + + await MatrixState.pangeaController.subscriptionController.reinitialize(); + } catch (err) { + if (err is MatrixException) { + _profileError = err.errorMessage; + } else { + _profileError = err.toLocalizedString(context); + } + } finally { + if (mounted) { + setState(() => _loadingProfile = false); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Row( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).planTrip), + ], + ), + ), + body: SafeArea( + child: Center( + child: _loadingProfile + ? const CircularProgressIndicator.adaptive() + : _profileError != null + ? const ErrorIndicator( + message: "Failed to create profile", + ) + : Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 350, + maxHeight: 600, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PangeaLogoSvg( + width: 100.0, + forceColor: theme.colorScheme.onSurface, + ), + Column( + spacing: 16.0, + children: [ + Text( + L10n.of(context).howAreYouTraveling, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ElevatedButton( + onPressed: () => context.go( + "/course/${widget.langCode}/private", + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).unlockPrivateTrip), + ], + ), + ), + ElevatedButton( + onPressed: () => context.go( + "/course/${widget.langCode}/public", + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.map_search), + Text(L10n.of(context).joinPublicTrip), + ], + ), + ), + ElevatedButton( + onPressed: () => context.go( + "/course/${widget.langCode}/own", + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).startOwnTrip), + ], + ), + ), + ListTile( + contentPadding: const EdgeInsets.all(0.0), + leading: const Icon(Icons.school), + title: Text( + L10n.of(context).tripPlanDesc, + style: theme.textTheme.labelLarge, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/private_trip_page.dart b/lib/pangea/login/pages/private_trip_page.dart new file mode 100644 index 000000000..18efb578c --- /dev/null +++ b/lib/pangea/login/pages/private_trip_page.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PrivateTripPage extends StatefulWidget { + final String langCode; + const PrivateTripPage({ + super.key, + required this.langCode, + }); + + @override + State createState() => PrivateTripPageState(); +} + +class PrivateTripPageState extends State { + final TextEditingController _codeController = TextEditingController(); + + @override + void initState() { + super.initState(); + _codeController.addListener(() => setState(() {})); + } + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + String get _code => _codeController.text.trim(); + + Future _submit() async { + if (_code.isEmpty) { + return; + } + + await MatrixState.pangeaController.classController.joinClasswithCode( + context, + _code, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Row( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).unlockPrivateTripTitle), + ], + ), + ), + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 350, + maxHeight: 600, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PangeaLogoSvg( + width: 100.0, + forceColor: theme.colorScheme.onSurface, + ), + Column( + spacing: 16.0, + children: [ + Text( + L10n.of(context).courseCode, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextFormField( + controller: _codeController, + decoration: InputDecoration( + hintText: L10n.of(context).courseCodeHint, + ), + onFieldSubmitted: (_) => _submit(), + ), + ElevatedButton( + onPressed: _code.isNotEmpty ? _submit : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).unlockMyTrip), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/public_trip_page.dart b/lib/pangea/login/pages/public_trip_page.dart new file mode 100644 index 000000000..59c0e079d --- /dev/null +++ b/lib/pangea/login/pages/public_trip_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PublicTripPage extends StatefulWidget { + final String langCode; + const PublicTripPage({ + super.key, + required this.langCode, + }); + + @override + State createState() => PublicTripPageState(); +} + +class PublicTripPageState extends State { + bool loading = true; + Object? error; + + LanguageLevelTypeEnum? languageLevelFilter; + LanguageModel? instructionLanguageFilter; + LanguageModel? targetLanguageFilter; + + @override + void initState() { + super.initState(); + + final target = PLanguageStore.byLangCode(widget.langCode); + if (target != null) { + setTargetLanguageFilter(target); + } + + final base = MatrixState.pangeaController.languageController.systemLanguage; + if (base != null) { + setInstructionLanguageFilter(base); + } + + _loadCourses(); + } + + void setLanguageLevelFilter(LanguageLevelTypeEnum? level) { + languageLevelFilter = level; + _loadCourses(); + } + + void setInstructionLanguageFilter(LanguageModel? language) { + instructionLanguageFilter = language; + _loadCourses(); + } + + void setTargetLanguageFilter(LanguageModel? language) { + targetLanguageFilter = language; + _loadCourses(); + } + + Future _loadCourses() async { + // TODO: add searching of public spaces + + try { + setState(() { + loading = true; + error = null; + }); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + error = e; + } finally { + setState(() => loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Row( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.map_outlined), + Text(L10n.of(context).browsePublicTrips), + ], + ), + ), + body: SafeArea( + child: Center( + child: Container( + padding: const EdgeInsets.all(30.0), + constraints: const BoxConstraints( + maxWidth: 450, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: [ + CoursePlanFilter( + value: instructionLanguageFilter, + onChanged: setInstructionLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.baseOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: + L10n.of(context).languageOfInstructionsLabel, + shortName: L10n.of(context).instructionsLanguage, + ), + CoursePlanFilter( + value: targetLanguageFilter, + onChanged: setTargetLanguageFilter, + items: MatrixState + .pangeaController.pLanguageStore.targetOptions, + displayname: (v) => + v.getDisplayName(context) ?? v.displayName, + enableSearch: true, + defaultName: L10n.of(context).targetLanguageLabel, + ), + CoursePlanFilter( + value: languageLevelFilter, + onChanged: setLanguageLevelFilter, + items: LanguageLevelTypeEnum.values, + displayname: (v) => v.string, + defaultName: L10n.of(context).cefrLevelLabel, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20.0), + Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: error != null + ? Center( + child: ErrorIndicator( + message: L10n.of(context).failedToLoadCourses, + ), + ) + : loading + ? const CircularProgressIndicator.adaptive() + : Text(L10n.of(context).noCoursesFound), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/login/pages/signup.dart b/lib/pangea/login/pages/signup.dart index 80be71bca..1f3c6bc37 100644 --- a/lib/pangea/login/pages/signup.dart +++ b/lib/pangea/login/pages/signup.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -13,7 +14,10 @@ import 'package:fluffychat/widgets/matrix.dart'; class SignupPage extends StatefulWidget { final bool withEmail; + final String langCode; + const SignupPage({ + required this.langCode, this.withEmail = false, super.key, }); @@ -154,7 +158,7 @@ class SignupPageController extends State { if (!valid) return; setState(() => loadingSignup = true); - await showFutureLoadingDialog( + final resp = await showFutureLoadingDialog( context: context, future: _signupFuture, onError: (e, s) { @@ -173,6 +177,8 @@ class SignupPageController extends State { : L10n.of(context).oopsSomethingWentWrong; }, ); + + if (!resp.isError) context.go("/course/${widget.langCode}"); } Future _signupFuture() async { @@ -201,6 +207,10 @@ class SignupPageController extends State { ), ); + if (!client.isLogged()) { + throw Exception(L10n.of(context).oopsSomethingWentWrong); + } + GoogleAnalytics.login("pangea", registerRes?.userId); if (displayname != localPart && client.userID != null) { diff --git a/lib/pangea/login/pages/signup_view.dart b/lib/pangea/login/pages/signup_view.dart index 789a1174b..de4bcd9fa 100644 --- a/lib/pangea/login/pages/signup_view.dart +++ b/lib/pangea/login/pages/signup_view.dart @@ -6,8 +6,6 @@ import 'package:go_router/go_router.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; -import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart'; import 'signup.dart'; @@ -15,71 +13,102 @@ class SignupPageView extends StatelessWidget { final SignupPageController controller; const SignupPageView(this.controller, {super.key}); + bool validator() { + return controller.formKey.currentState?.validate() ?? false; + } + @override Widget build(BuildContext context) { - bool validator() { - return controller.formKey.currentState?.validate() ?? false; - } - + final theme = Theme.of(context); return Form( key: controller.formKey, - child: PangeaLoginScaffold( - children: [ - FullWidthButton( - title: L10n.of(context).signUpWithEmail, - onPressed: () { - if (!validator()) return; - context.go( - '/home/signup/email', - ); - }, - icon: PangeaLogoSvg( - width: 20, - forceColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - PangeaSsoButton( - provider: SSOProvider.google, - title: L10n.of(context).signUpWithGoogle, - setLoading: controller.setLoadingSSO, - loading: controller.loadingGoogleSSO, - validator: validator, - ), - PangeaSsoButton( - provider: SSOProvider.apple, - title: L10n.of(context).signUpWithApple, - setLoading: controller.setLoadingSSO, - loading: controller.loadingAppleSSO, - validator: validator, - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 8.0, - ), - child: RichText( - textAlign: TextAlign.justify, - text: TextSpan( - text: L10n.of(context).byUsingPangeaChat, + child: Scaffold( + appBar: AppBar(), + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 600, + ), + child: Column( + spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.end, children: [ - TextSpan( - text: L10n.of(context).termsAndConditions, - style: const TextStyle( - decoration: TextDecoration.underline, + Text( + L10n.of(context).signupOption, + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, ), ), - TextSpan( - text: L10n.of(context).andCertifyIAmAtLeast13YearsOfAge, + PangeaSsoButton( + provider: SSOProvider.google, + setLoading: controller.setLoadingSSO, + loading: controller.loadingGoogleSSO, + validator: validator, + ), + PangeaSsoButton( + provider: SSOProvider.apple, + setLoading: controller.setLoadingSSO, + loading: controller.loadingAppleSSO, + validator: validator, + ), + ElevatedButton( + onPressed: () => context.go( + '/home/languages/${controller.widget.langCode}/email', + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PangeaLogoSvg( + width: 20, + forceColor: Theme.of(context).colorScheme.onSurface, + ), + Text(L10n.of(context).withEmail), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + text: L10n.of(context).byUsingPangeaChat, + children: [ + TextSpan( + text: L10n.of(context).termsAndConditions, + style: TextStyle( + decoration: TextDecoration.underline, + color: theme.colorScheme.primary, + ), + ), + TextSpan( + text: L10n.of(context) + .andCertifyIAmAtLeast13YearsOfAge, + ), + ], + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), ), ], - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - ), ), ), ), - ], + ), ), ); } diff --git a/lib/pangea/login/pages/signup_with_email_view.dart b/lib/pangea/login/pages/signup_with_email_view.dart index ce7b75b25..9f05c0714 100644 --- a/lib/pangea/login/pages/signup_with_email_view.dart +++ b/lib/pangea/login/pages/signup_with_email_view.dart @@ -3,9 +3,6 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart'; -import 'package:fluffychat/pangea/login/pages/pangea_login_scaffold.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; import 'signup.dart'; class SignupWithEmailView extends StatelessWidget { @@ -14,55 +11,93 @@ class SignupWithEmailView extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Form( key: controller.formKey, - child: PangeaLoginScaffold( - children: [ - FullWidthTextField( - hintText: L10n.of(context).yourUsername, - textInputAction: TextInputAction.next, - validator: (text) { - if (text == null || text.isEmpty) { - return L10n.of(context).pleaseChooseAUsername; - } - return null; - }, - controller: controller.usernameController, - ), - FullWidthTextField( - hintText: L10n.of(context).yourEmail, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.emailAddress, - validator: controller.emailTextFieldValidator, - controller: controller.emailController, - ), - FullWidthTextField( - hintText: L10n.of(context).password, - textInputAction: TextInputAction.done, - obscureText: !controller.showPassword, - validator: controller.password1TextFieldValidator, - controller: controller.passwordController, - onSubmitted: controller.enableSignUp ? controller.signup : null, - suffix: IconButton( - icon: Icon( - controller.showPassword - ? Icons.visibility_off - : Icons.visibility, + child: Scaffold( + appBar: AppBar(), + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 600, + ), + child: Column( + spacing: 24.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context).yourUsername, + ), + textInputAction: TextInputAction.next, + validator: (text) { + if (text == null || text.isEmpty) { + return L10n.of(context).pleaseChooseAUsername; + } + return null; + }, + controller: controller.usernameController, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + TextFormField( + textInputAction: TextInputAction.next, + keyboardType: TextInputType.emailAddress, + validator: controller.emailTextFieldValidator, + controller: controller.emailController, + decoration: InputDecoration( + hintText: L10n.of(context).yourEmail, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + TextFormField( + textInputAction: TextInputAction.done, + obscureText: !controller.showPassword, + validator: controller.password1TextFieldValidator, + controller: controller.passwordController, + onFieldSubmitted: + controller.enableSignUp ? controller.signup : null, + decoration: InputDecoration( + hintText: L10n.of(context).password, + suffixIcon: IconButton( + icon: Icon( + controller.showPassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: controller.toggleShowPassword, + ), + isDense: true, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ElevatedButton( + onPressed: + controller.enableSignUp ? controller.signup : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).createAccount), + ], + ), + ), + ], ), - onPressed: controller.toggleShowPassword, ), ), - FullWidthButton( - title: L10n.of(context).signUp, - icon: PangeaLogoSvg( - width: 20, - forceColor: Theme.of(context).colorScheme.onPrimary, - ), - onPressed: controller.enableSignUp ? controller.signup : null, - loading: controller.loadingSignup, - enabled: controller.enableSignUp, - ), - ], + ), ), ); } diff --git a/lib/pangea/login/widgets/p_sso_button.dart b/lib/pangea/login/widgets/p_sso_button.dart index f1127f4d7..97994375c 100644 --- a/lib/pangea/login/widgets/p_sso_button.dart +++ b/lib/pangea/login/widgets/p_sso_button.dart @@ -6,7 +6,6 @@ import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pangea/login/utils/sso_login_action.dart'; -import 'package:fluffychat/pangea/login/widgets/full_width_button.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; enum SSOProvider { google, apple } @@ -21,15 +20,6 @@ extension on SSOProvider { } } - String get name { - switch (this) { - case SSOProvider.google: - return "Google"; - case SSOProvider.apple: - return "Apple"; - } - } - String get asset { switch (this) { case SSOProvider.google: @@ -38,10 +28,19 @@ extension on SSOProvider { return "assets/pangea/apple.svg"; } } + + String description(BuildContext context) { + switch (this) { + case SSOProvider.google: + return L10n.of(context).withGoogle; + case SSOProvider.apple: + return L10n.of(context).withApple; + } + } } class PangeaSsoButton extends StatelessWidget { - final String title; + final String? title; final SSOProvider provider; final Function(bool, SSOProvider) setLoading; @@ -49,9 +48,9 @@ class PangeaSsoButton extends StatelessWidget { final bool? Function()? validator; const PangeaSsoButton({ - required this.title, required this.provider, required this.setLoading, + this.title, this.loading = false, this.validator, super.key, @@ -81,19 +80,32 @@ class PangeaSsoButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FullWidthButton( - depressed: loading, - loading: loading, - title: title, - icon: SvgPicture.asset( - provider.asset, - height: 20, - width: 20, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onPrimary, - BlendMode.srcIn, + final theme = Theme.of(context); + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide( + width: 1, + color: theme.colorScheme.onSurface, ), ), + child: Row( + spacing: 8.0, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + provider.asset, + height: 20, + width: 20, + colorFilter: ColorFilter.mode( + theme.colorScheme.onSurface, + BlendMode.srcIn, + ), + ), + Text(title ?? provider.description(context)), + ], + ), onPressed: () { if (validator != null) { final valid = validator!.call() ?? false; diff --git a/lib/pangea/toolbar/widgets/icon_rain.dart b/lib/pangea/toolbar/widgets/icon_rain.dart index 7b9d41ada..b6dd09aff 100644 --- a/lib/pangea/toolbar/widgets/icon_rain.dart +++ b/lib/pangea/toolbar/widgets/icon_rain.dart @@ -67,9 +67,11 @@ class _IconRainState extends State with TickerProviderStateMixin { swayFrequency: widget.swayFrequency, fadeMidpoint: 0.4 + _random.nextDouble() * 0.2, // 40-60% down onComplete: () { - setState(() { - _icons.removeWhere((i) => i.key == _icons.first.key); - }); + if (mounted) { + setState(() { + _icons.removeWhere((i) => i.key == _icons.first.key); + }); + } }, ), ); @@ -98,9 +100,11 @@ class _IconRainState extends State with TickerProviderStateMixin { swayFrequency: widget.swayFrequency, fadeMidpoint: 0.4 + _random.nextDouble() * 0.2, // 40-60% down onComplete: () { - setState(() { - _icons.removeWhere((i) => i.key == _icons.first.key); - }); + if (mounted) { + setState(() { + _icons.removeWhere((i) => i.key == _icons.first.key); + }); + } }, ), ); diff --git a/lib/pangea/user/controllers/user_controller.dart b/lib/pangea/user/controllers/user_controller.dart index cc56db9d7..c2d147366 100644 --- a/lib/pangea/user/controllers/user_controller.dart +++ b/lib/pangea/user/controllers/user_controller.dart @@ -265,7 +265,7 @@ class UserController { } /// Checks if user data is available and the user's l2 is set. - Future get isUserDataAvailableAndL2Set async { + Future get isUserL2Set async { try { // the function fetchUserModel() uses a completer, so it shouldn't // re-call the endpoint if it has already been called diff --git a/lib/pangea/user/utils/p_logout.dart b/lib/pangea/user/utils/p_logout.dart index c0f0b7ff4..626d5f05e 100644 --- a/lib/pangea/user/utils/p_logout.dart +++ b/lib/pangea/user/utils/p_logout.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -24,14 +28,28 @@ void pLogoutAction( } } - final matrix = Matrix.of(context); + final client = Matrix.of(context).client; // before wiping out locally cached construct data, save it to the server await MatrixState.pangeaController.putAnalytics .sendLocalAnalyticsToAnalyticsRoom(onLogout: true); + final redirect = client.onLoginStateChanged.stream + .where((state) => state != LoginState.loggedIn) + .first + .then( + (_) { + final route = FluffyChatApp.router.state.fullPath; + if (route == null || !route.contains("/home")) { + context.go("/home"); + } + }, + ).timeout(const Duration(seconds: 30)); + await showFutureLoadingDialog( context: context, - future: () => matrix.client.logout(), + future: () => client.logout(), ); + + await redirect; } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 268af46b2..8be849371 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -195,7 +195,9 @@ class MatrixState extends State with WidgetsBindingObserver { ); _registerSubs(_loginClientCandidate!.clientName); _loginClientCandidate = null; - FluffyChatApp.router.go('/rooms'); + // #Pangea + // FluffyChatApp.router.go('/rooms'); + // Pangea# }); // #Pangea candidate.homeserver = Uri.parse("https://${AppConfig.defaultHomeserver}"); @@ -378,8 +380,13 @@ class MatrixState extends State with WidgetsBindingObserver { FluffyChatApp.router.go('/rooms'); } } else { - FluffyChatApp.router - .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + // #Pangea + if (state != LoginState.loggedIn) { + FluffyChatApp.router.go('/home'); + } + // FluffyChatApp.router + // .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + // Pangea# } }); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler);