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
This commit is contained in:
parent
e2be29d211
commit
3ed4add04e
26 changed files with 1831 additions and 674 deletions
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Login> {
|
|||
@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#
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ class CoursePlanFilter<T> extends StatefulWidget {
|
|||
|
||||
final bool enableSearch;
|
||||
|
||||
final double fontSize;
|
||||
final double iconSize;
|
||||
|
||||
const CoursePlanFilter({
|
||||
super.key,
|
||||
required this.value,
|
||||
|
|
@ -25,8 +22,6 @@ class CoursePlanFilter<T> 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<T> extends State<CoursePlanFilter<T>> {
|
|||
child: DropdownButton2<T>(
|
||||
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<T> extends State<CoursePlanFilter<T>> {
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
63
lib/pangea/course_creation/course_search_provider.dart
Normal file
63
lib/pangea/course_creation/course_search_provider.dart
Normal file
|
|
@ -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<T extends StatefulWidget> on State<T> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
List<CoursePlanModel> 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<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NewCourse> createState() => NewCourseController();
|
||||
}
|
||||
|
||||
class NewCourseController extends State<NewCourse> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
List<CoursePlanModel> courses = [];
|
||||
|
||||
LanguageLevelTypeEnum? languageLevelFilter;
|
||||
LanguageModel? instructionLanguageFilter;
|
||||
LanguageModel? targetLanguageFilter;
|
||||
|
||||
class NewCourseController extends State<NewCourse> 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<LanguageModel>(
|
||||
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<LanguageModel>(
|
||||
value: targetLanguageFilter,
|
||||
onChanged: setTargetLanguageFilter,
|
||||
items: MatrixState
|
||||
.pangeaController.pLanguageStore.targetOptions,
|
||||
displayname: (v) =>
|
||||
v.getDisplayName(context) ?? v.displayName,
|
||||
enableSearch: true,
|
||||
defaultName: L10n.of(context).targetLanguageLabel,
|
||||
),
|
||||
CoursePlanFilter<LanguageLevelTypeEnum>(
|
||||
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<void> _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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LanguageLevelTypeEnum>(
|
||||
value: controller.languageLevelFilter,
|
||||
onChanged: controller.setLanguageLevelFilter,
|
||||
items: LanguageLevelTypeEnum.values,
|
||||
displayname: (v) => v.string,
|
||||
fontSize: descFontSize,
|
||||
iconSize: iconSize,
|
||||
defaultName: L10n.of(context).cefrLevelLabel,
|
||||
),
|
||||
CoursePlanFilter<LanguageModel>(
|
||||
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<LanguageModel>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,146 +11,55 @@ class PAuthGaurd {
|
|||
static bool isPublicLeaving = false;
|
||||
static PangeaController? pController;
|
||||
|
||||
/// Redirect for /home routes
|
||||
static FutureOr<String?> 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<String?> 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<String?> _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<void> onPublicEnter() async {
|
||||
// final bool setDob =
|
||||
// await pController!.userController.isUserDataAvailableAndDateOfBirthSet;
|
||||
// if (_isLogged != null && _isLogged! && setDob) {
|
||||
// vRedirector.to('/rooms');
|
||||
// }
|
||||
// }
|
||||
|
||||
// static Future<void> 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<void> onPublicLeave(
|
||||
// VRedirector vRedirector,
|
||||
// Function(Map<String, String> 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<void> onPrivateUpdate(VRedirector vRedirector) async {
|
||||
// if (_isLogged == null) {
|
||||
// return;
|
||||
// }
|
||||
// final Map<String, String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
145
lib/pangea/login/pages/language_selection_page.dart
Normal file
145
lib/pangea/login/pages/language_selection_page.dart
Normal file
|
|
@ -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<LanguageSelectionPage> createState() => LanguageSelectionPageState();
|
||||
}
|
||||
|
||||
class LanguageSelectionPageState extends State<LanguageSelectionPage> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
102
lib/pangea/login/pages/login_options_view.dart
Normal file
102
lib/pangea/login/pages/login_options_view.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<LoginOrSignupView> {
|
||||
Client? client;
|
||||
List<AppConfigOverride> _overrides = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOverrides();
|
||||
|
||||
Matrix.of(context).getLoginClient().then((c) {
|
||||
if (mounted) setState(() => client = c);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadOverrides() async {
|
||||
|
|
@ -52,30 +45,94 @@ class LoginOrSignupViewState extends State<LoginOrSignupView> {
|
|||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
204
lib/pangea/login/pages/new_trip_page.dart
Normal file
204
lib/pangea/login/pages/new_trip_page.dart
Normal file
|
|
@ -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<NewTripPage> createState() => NewTripPageState();
|
||||
}
|
||||
|
||||
class NewTripPageState extends State<NewTripPage> 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<LanguageModel>(
|
||||
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<LanguageModel>(
|
||||
value: targetLanguageFilter,
|
||||
onChanged: setTargetLanguageFilter,
|
||||
items: MatrixState
|
||||
.pangeaController.pLanguageStore.targetOptions,
|
||||
displayname: (v) =>
|
||||
v.getDisplayName(context) ?? v.displayName,
|
||||
enableSearch: true,
|
||||
defaultName: L10n.of(context).targetLanguageLabel,
|
||||
),
|
||||
CoursePlanFilter<LanguageLevelTypeEnum>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
256
lib/pangea/login/pages/plan_trip_page.dart
Normal file
256
lib/pangea/login/pages/plan_trip_page.dart
Normal file
|
|
@ -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<PlanTripPage> createState() => PlanTripPageState();
|
||||
}
|
||||
|
||||
class PlanTripPageState extends State<PlanTripPage> {
|
||||
bool _loadingProfile = true;
|
||||
Object? _profileError;
|
||||
|
||||
final List<String> 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<void> _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<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/pangea/login/pages/private_trip_page.dart
Normal file
117
lib/pangea/login/pages/private_trip_page.dart
Normal file
|
|
@ -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<PrivateTripPage> createState() => PrivateTripPageState();
|
||||
}
|
||||
|
||||
class PrivateTripPageState extends State<PrivateTripPage> {
|
||||
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<void> _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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/pangea/login/pages/public_trip_page.dart
Normal file
164
lib/pangea/login/pages/public_trip_page.dart
Normal file
|
|
@ -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<PublicTripPage> createState() => PublicTripPageState();
|
||||
}
|
||||
|
||||
class PublicTripPageState extends State<PublicTripPage> {
|
||||
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<void> _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<LanguageModel>(
|
||||
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<LanguageModel>(
|
||||
value: targetLanguageFilter,
|
||||
onChanged: setTargetLanguageFilter,
|
||||
items: MatrixState
|
||||
.pangeaController.pLanguageStore.targetOptions,
|
||||
displayname: (v) =>
|
||||
v.getDisplayName(context) ?? v.displayName,
|
||||
enableSearch: true,
|
||||
defaultName: L10n.of(context).targetLanguageLabel,
|
||||
),
|
||||
CoursePlanFilter<LanguageLevelTypeEnum>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SignupPage> {
|
|||
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<SignupPage> {
|
|||
: L10n.of(context).oopsSomethingWentWrong;
|
||||
},
|
||||
);
|
||||
|
||||
if (!resp.isError) context.go("/course/${widget.langCode}");
|
||||
}
|
||||
|
||||
Future<void> _signupFuture() async {
|
||||
|
|
@ -201,6 +207,10 @@ class SignupPageController extends State<SignupPage> {
|
|||
),
|
||||
);
|
||||
|
||||
if (!client.isLogged()) {
|
||||
throw Exception(L10n.of(context).oopsSomethingWentWrong);
|
||||
}
|
||||
|
||||
GoogleAnalytics.login("pangea", registerRes?.userId);
|
||||
|
||||
if (displayname != localPart && client.userID != null) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -67,9 +67,11 @@ class _IconRainState extends State<IconRain> 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<IconRain> 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);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class UserController {
|
|||
}
|
||||
|
||||
/// Checks if user data is available and the user's l2 is set.
|
||||
Future<bool> get isUserDataAvailableAndL2Set async {
|
||||
Future<bool> get isUserL2Set async {
|
||||
try {
|
||||
// the function fetchUserModel() uses a completer, so it shouldn't
|
||||
// re-call the endpoint if it has already been called
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,9 @@ class MatrixState extends State<Matrix> 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<Matrix> 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue