feat: auth pages improvement for mobile and desktop platform (#3217)

This commit is contained in:
Yijing Huang 2023-09-11 21:32:26 -05:00 committed by GitHub
parent 71071b60b2
commit f639d51c11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1541 additions and 843 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,4 +1,4 @@
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@ -7,15 +7,15 @@ import 'base.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byType(GoogleSignUpButton));
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
}
Future<void> tapSignInAsGuest() async {
await tapButton(find.byType(SignInAsGuestButton));
await tapButton(find.byType(SignInAnonymousButton));
}
void expectToSeeGoogleLoginButton() {
expect(find.byType(GoogleSignUpButton), findsOneWidget);
expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget);
}
void assertSwitchValue(Finder finder, bool value) {

View File

@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';

View File

@ -50,6 +50,8 @@ PODS:
- Toast
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@ -66,6 +68,8 @@ PODS:
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
@ -83,11 +87,13 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
@ -119,6 +125,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@ -129,6 +137,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
@ -146,6 +156,7 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
@ -153,6 +164,7 @@ SPEC CHECKSUMS:
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
super_native_extensions: 49e897b6039bb784226e7354e502a3c68e1659b5
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4

View File

@ -16,6 +16,17 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View File

@ -0,0 +1,47 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({
super.key,
required this.userProfile,
required this.workspaceSetting,
});
static const routeName = "/MobileHomeScreen";
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("MobileHomeScreen"),
),
// TODO(yijing): implement home page later
body: Center(
child: Column(
children: [
const Text(
'User',
),
Text(
userProfile.toString(),
),
Text('Workspace name: ${workspaceSetting.workspace.name}'),
ElevatedButton(
onPressed: () async {
await getIt<AuthService>().signOut();
runAppFlowy();
},
child: const Text('Logout'),
)
],
),
),
);
}
}

View File

@ -107,7 +107,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
() => SignUpBloc(getIt<AuthService>()),
);
getIt.registerFactory<SplashRoute>(() => SplashRoute());
getIt.registerFactory<SplashRouter>(() => SplashRouter());
getIt.registerFactory<EditPanelBloc>(() => EditPanelBloc());
getIt.registerFactory<SplashBloc>(() => SplashBloc());
getIt.registerLazySingleton<NetworkListener>(() => NetworkListener());
@ -122,8 +122,8 @@ void _resolveHomeDeps(GetIt getIt) {
(user, _) => UserListener(userProfile: user),
);
getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
(user, _) => WelcomeBloc(
getIt.registerFactoryParam<WorkspaceBloc, UserProfilePB, void>(
(user, _) => WorkspaceBloc(
userService: UserBackendService(userId: user.id),
),
);

View File

@ -1,6 +1,6 @@
import 'package:appflowy/startup/launch_configuration.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/splash_screen.dart';
import 'package:appflowy/user/presentation/screens/splash_screen.dart';
import 'package:flutter/material.dart';
class FlowyApp implements EntryPoint {

View File

@ -0,0 +1,33 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
Log.error(error);
switch (error.code) {
case ErrorCode.WorkspaceDataNotSync:
final userFolder = UserFolderPB.fromBuffer(error.payload);
getIt<AuthRouter>().pushWorkspaceErrorScreen(context, userFolder, error);
break;
case ErrorCode.InvalidEncryptSecret:
showSnapBar(
context,
error.msg,
);
break;
default:
showSnapBar(
context,
error.msg,
onClosed: () {
getIt<AuthService>().signOut();
runAppFlowy();
},
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:appflowy/user/presentation/helpers/helpers.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:flutter/material.dart';
void handleSuccessOrFail(
Either<UserProfilePB, FlowyError> result,
BuildContext context,
AuthRouter router,
) {
result.fold(
(user) {
if (user.encryptionType == EncryptionTypePB.Symmetric) {
router.pushEncryptionScreen(context, user);
} else {
router.pushHomeScreen(context, user);
}
},
(error) {
handleOpenWorkspaceError(context, error);
},
);
}

View File

@ -0,0 +1,2 @@
export 'handle_open_workspace_error.dart';
export 'handle_success_or_fail.dart';

View File

@ -0,0 +1,4 @@
export 'screens/screens.dart';
export 'widgets/widgets.dart';
export 'historical_user.dart';
export 'router.dart';

View File

@ -1,9 +1,8 @@
import 'package:appflowy/mobile/presentation/mobile_home_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/sign_up_screen.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/user/presentation/welcome_screen.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -13,64 +12,77 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter/material.dart';
import 'encrypt_secret_screen.dart';
import 'workspace_error_screen.dart';
const routerNameRoot = '/';
const routerNameSignUp = '/signUp';
const routerNameSignIn = '/signIn';
const routerNameSkipLogIn = '/skipLogIn';
const routerNameWelcome = '/welcome';
const routerNameHome = '/home';
import 'package:appflowy/util/platform_extension.dart';
class AuthRouter {
void pushForgetPasswordScreen(BuildContext context) {}
void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) {
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
void pushWorkspaceStartScreen(
BuildContext context,
UserProfilePB userProfile,
) {
getIt<SplashRouter>().pushWorkspaceStartScreen(context, userProfile);
}
void pushSignUpScreen(BuildContext context) {
Navigator.of(context).push(
PageRoutes.fade(
() => SignUpScreen(router: getIt<AuthRouter>()),
const RouteSettings(name: routerNameSignUp),
),
);
}
void pushHomeScreenWithWorkSpace(
BuildContext context,
UserProfilePB profile,
WorkspaceSettingPB workspaceSetting,
) {
Navigator.push(
context,
PageRoutes.fade(
() => HomeScreen(
profile,
workspaceSetting,
key: ValueKey(profile.id),
),
const RouteSettings(name: routerNameHome),
RouteDurations.slow.inMilliseconds * .001,
const RouteSettings(name: SignUpScreen.routeName),
),
);
}
/// Navigates to the home screen based on the current workspace and platform.
///
/// This function takes in a [BuildContext] and a [UserProfilePB] object to
/// determine the user's settings and then navigate to the appropriate home screen
/// (`MobileHomeScreen` for mobile platforms, `DesktopHomeScreen` for others).
///
/// It first fetches the current workspace settings using [FolderEventGetCurrentWorkspace].
/// If the workspace settings are successfully fetched, it navigates to the home screen.
/// If there's an error, it defaults to the workspace start screen.
///
/// @param [context] BuildContext for navigating to the appropriate screen.
/// @param [userProfile] UserProfilePB object containing the details of the current user.
///
Future<void> pushHomeScreen(
BuildContext context,
UserProfilePB userProfile,
) async {
final result = await FolderEventGetCurrentWorkspace().send();
result.fold(
(workspaceSettingPB) => pushHomeScreenWithWorkSpace(
context,
userProfile,
workspaceSettingPB,
),
(r) => pushWelcomeScreen(context, userProfile),
(workspaceSetting) {
if (PlatformExtension.isMobile) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute<void>(
builder: (BuildContext context) => MobileHomeScreen(
key: ValueKey(userProfile.id),
userProfile: userProfile,
workspaceSetting: workspaceSetting,
),
),
// pop up all the pages until [SplashScreen]
(route) => route.settings.name == SplashScreen.routeName,
);
} else {
Navigator.push(
context,
PageRoutes.fade(
() => DesktopHomeScreen(
key: ValueKey(userProfile.id),
userProfile: userProfile,
workspaceSetting: workspaceSetting,
),
const RouteSettings(
name: DesktopHomeScreen.routeName,
),
RouteDurations.slow.inMilliseconds * .001,
),
);
}
},
(error) => pushWorkspaceStartScreen(context, userProfile),
);
}
@ -85,7 +97,7 @@ class AuthRouter {
user: userProfile,
key: ValueKey(userProfile.id),
),
const RouteSettings(name: routerNameWelcome),
const RouteSettings(name: EncryptSecretScreen.routeName),
RouteDurations.slow.inMilliseconds * .001,
),
);
@ -103,23 +115,23 @@ class AuthRouter {
await Navigator.of(context).push(
PageRoutes.fade(
() => screen,
const RouteSettings(name: routerNameWelcome),
const RouteSettings(name: WorkspaceErrorScreen.routeName),
RouteDurations.slow.inMilliseconds * .001,
),
);
}
}
class SplashRoute {
Future<void> pushWelcomeScreen(
class SplashRouter {
Future<void> pushWorkspaceStartScreen(
BuildContext context,
UserProfilePB userProfile,
) async {
final screen = WelcomeScreen(userProfile: userProfile);
final screen = WorkspaceStartScreen(userProfile: userProfile);
await Navigator.of(context).push(
PageRoutes.fade(
() => screen,
const RouteSettings(name: routerNameWelcome),
const RouteSettings(name: WorkspaceStartScreen.routeName),
RouteDurations.slow.inMilliseconds * .001,
),
);
@ -138,18 +150,35 @@ class SplashRoute {
UserProfilePB userProfile,
WorkspaceSettingPB workspaceSetting,
) {
Navigator.push(
context,
PageRoutes.fade(
() => HomeScreen(
userProfile,
workspaceSetting,
key: ValueKey(userProfile.id),
if (PlatformExtension.isMobile) {
Navigator.pushAndRemoveUntil<void>(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => MobileHomeScreen(
key: ValueKey(userProfile.id),
userProfile: userProfile,
workspaceSetting: workspaceSetting,
),
),
const RouteSettings(name: routerNameWelcome),
RouteDurations.slow.inMilliseconds * .001,
),
);
// pop up all the pages until [SplashScreen]
(route) => route.settings.name == SplashScreen.routeName,
);
} else {
Navigator.push(
context,
PageRoutes.fade(
() => DesktopHomeScreen(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
key: ValueKey(userProfile.id),
),
const RouteSettings(
name: DesktopHomeScreen.routeName,
),
RouteDurations.slow.inMilliseconds * .001,
),
);
}
}
void pushSignInScreen(BuildContext context) {
@ -157,7 +186,7 @@ class SplashRoute {
context,
PageRoutes.fade(
() => SignInScreen(router: getIt<AuthRouter>()),
const RouteSettings(name: routerNameSignIn),
const RouteSettings(name: SignInScreen.routeName),
RouteDurations.slow.inMilliseconds * .001,
),
);
@ -171,7 +200,7 @@ class SplashRoute {
router: getIt<AuthRouter>(),
authService: getIt<AuthService>(),
),
const RouteSettings(name: routerNameSkipLogIn),
const RouteSettings(name: SkipLogInScreen.routeName),
RouteDurations.slow.inMilliseconds * .001,
),
);

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/helpers/helpers.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@ -9,9 +9,10 @@ import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/encrypt_secret_bloc.dart';
import '../../application/encrypt_secret_bloc.dart';
class EncryptSecretScreen extends StatefulWidget {
static const routeName = "/EncryptSecretScreen";
final UserProfilePB user;
const EncryptSecretScreen({required this.user, super.key});

View File

@ -0,0 +1,6 @@
export 'sign_in_screen/sign_in_screen.dart';
export 'skip_log_in_screen.dart';
export 'splash_screen.dart';
export 'sign_up_screen.dart';
export 'encrypt_secret_screen.dart';
export 'workspace_error_screen.dart';

View File

@ -0,0 +1,238 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class DesktopSignInScreen extends StatelessWidget {
const DesktopSignInScreen({super.key, required this.isLoading});
final bool isLoading;
@override
Widget build(BuildContext context) {
const indicatorMinHeight = 4.0;
return Scaffold(
appBar: const PreferredSize(
preferredSize: Size(double.infinity, 60),
child: MoveWindowDetector(),
),
body: Center(
child: AuthFormContainer(
children: [
FlowyLogoTitle(
title: LocaleKeys.welcomeText.tr(),
logoSize: const Size(60, 60),
),
const VSpace(30),
// Email and password. don't support yet.
/*
...[
const EmailTextField(),
const VSpace(5),
const PasswordTextField(),
const VSpace(20),
const LoginButton(),
const VSpace(10),
const VSpace(10),
SignUpPrompt(router: router),
],
*/
const SignInAnonymousButton(),
// third-party sign in.
const VSpace(20),
const _OrDivider(),
const VSpace(10),
const ThirdPartySignInButtons(),
const VSpace(20),
// loading status
const VSpace(indicatorMinHeight),
isLoading
? const LinearProgressIndicator(
value: null,
minHeight: indicatorMinHeight,
)
: const VSpace(indicatorMinHeight),
// add the same space when there's no loading status.
// ConstrainedBox(
// constraints: const BoxConstraints(maxHeight: 140),
// child: HistoricalUserList(
// didOpenUser: () async {
// await FlowyRunner.run(
// FlowyApp(),
// integrationEnv(),
// );
// },
// ),
// ),
const VSpace(20),
],
),
),
);
}
}
class _OrDivider extends StatelessWidget {
const _OrDivider();
@override
Widget build(BuildContext context) {
return Row(
children: [
const Flexible(
child: Divider(
thickness: 1,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: FlowyText.regular(LocaleKeys.signIn_or.tr()),
),
const Flexible(
child: Divider(
thickness: 1,
),
),
],
);
}
}
// The following code is migrated from previous signInScreen.dart(for desktop)
// We may need this later when sign up&in feature is ready
// class SignUpPrompt extends StatelessWidget {
// const SignUpPrompt({
// super.key,
// required this.router,
// }) ;
// final AuthRouter router;
// @override
// Widget build(BuildContext context) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// FlowyText.medium(
// LocaleKeys.signIn_dontHaveAnAccount.tr(),
// color: Theme.of(context).hintColor,
// ),
// TextButton(
// style: TextButton.styleFrom(
// textStyle: Theme.of(context).textTheme.bodyMedium,
// ),
// onPressed: () => router.pushSignUpScreen(context),
// child: Text(
// LocaleKeys.signUp_buttonText.tr(),
// style: TextStyle(color: Theme.of(context).colorScheme.primary),
// ),
// ),
// ForgetPasswordButton(router: router),
// ],
// );
// }
// }
// class LoginButton extends StatelessWidget {
// const LoginButton({
// super.key
// }) ;
// @override
// Widget build(BuildContext context) {
// return RoundedTextButton(
// title: LocaleKeys.signIn_loginButtonText.tr(),
// height: 48,
// borderRadius: Corners.s10Border,
// onPressed: () => context
// .read<SignInBloc>()
// .add(const SignInEvent.signedInWithUserEmailAndPassword()),
// );
// }
// }
// class ForgetPasswordButton extends StatelessWidget {
// const ForgetPasswordButton({
// super.key
// required this.router,
// }) ;
// final AuthRouter router;
// @override
// Widget build(BuildContext context) {
// return TextButton(
// style: TextButton.styleFrom(
// textStyle: Theme.of(context).textTheme.bodyMedium,
// ),
// onPressed: () {
// throw UnimplementedError();
// },
// child: Text(
// LocaleKeys.signIn_forgotPassword.tr(),
// style: TextStyle(color: Theme.of(context).colorScheme.primary),
// ),
// );
// }
// }
// class PasswordTextField extends StatelessWidget {
// const PasswordTextField({
// super.key
// }) ;
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<SignInBloc, SignInState>(
// buildWhen: (previous, current) =>
// previous.passwordError != current.passwordError,
// builder: (context, state) {
// return RoundedInputField(
// obscureText: true,
// obscureIcon: const FlowySvg(FlowySvgs.hide_m),
// obscureHideIcon: const FlowySvg(FlowySvgs.show_m),
// hintText: LocaleKeys.signIn_passwordHint.tr(),
// errorText: context
// .read<SignInBloc>()
// .state
// .passwordError
// .fold(() => "", (error) => error),
// onChanged: (value) => context
// .read<SignInBloc>()
// .add(SignInEvent.passwordChanged(value)),
// );
// },
// );
// }
// }
// class EmailTextField extends StatelessWidget {
// const EmailTextField({
// super.key
// }) ;
// @override
// Widget build(BuildContext context) {
// return BlocBuilder<SignInBloc, SignInState>(
// buildWhen: (previous, current) =>
// previous.emailError != current.emailError,
// builder: (context, state) {
// return RoundedInputField(
// hintText: LocaleKeys.signIn_emailHint.tr(),
// errorText: context
// .read<SignInBloc>()
// .state
// .emailError
// .fold(() => "", (error) => error),
// onChanged: (value) =>
// context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
// );
// },
// );
// }
// }

View File

@ -0,0 +1,91 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileSignInScreen extends StatelessWidget {
const MobileSignInScreen({super.key, required this.isLoading});
final bool isLoading;
@override
Widget build(BuildContext context) {
const spacing = 16;
// Welcome to Appflowy
final welcomeString = LocaleKeys.welcomeText.tr();
final style = Theme.of(context);
return Scaffold(
resizeToAvoidBottomInset: false,
body: isLoading
? // TODO(yijing): improve loading effect in the future
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Signing in...'),
VSpace(8),
CircularProgressIndicator(),
],
),
)
: Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 30),
child: Column(
children: [
const Spacer(
flex: 4,
),
const FlowySvg(
FlowySvgs.flowy_logo_xl,
size: Size.square(64),
blendMode: null,
),
const VSpace(spacing * 2),
// Welcome to
Text(
welcomeString.substring(0, welcomeString.length - 8),
style: style.textTheme.displayMedium,
textAlign: TextAlign.center,
),
// Appflowy
Text(
welcomeString.substring(welcomeString.length - 8),
style: style.textTheme.displayLarge,
textAlign: TextAlign.center,
),
const VSpace(16),
// TODO(yijing): confirm the subtitle before release app
Text(
'You are in charge of your data and customizations.',
style: style.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const Spacer(
flex: 2,
),
const SignInAnonymousButton(),
const VSpace(16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
LocaleKeys.signIn_or.tr(),
style: style.textTheme.bodyMedium,
),
),
const Expanded(child: Divider()),
],
),
const VSpace(16),
const ThirdPartySignInButtons(),
const VSpace(16),
],
),
),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../helpers/helpers.dart';
class SignInScreen extends StatelessWidget {
const SignInScreen({
super.key,
required this.router,
});
static const routeName = '/SignInScreen';
final AuthRouter router;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<SignInBloc>(),
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail.fold(
() => null,
(result) => handleSuccessOrFail(result, context, router),
);
},
builder: (context, state) {
// When user is logining through 3rd party, a loading widget will appear on the screen. [isLoading] is used to control it is on or not.
final isLoading = context.read<SignInBloc>().state.isSubmitting;
if (PlatformExtension.isMobile) {
return MobileSignInScreen(
isLoading: isLoading,
);
}
return DesktopSignInScreen(
isLoading: isLoading,
);
},
),
);
}
}

View File

@ -0,0 +1,85 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SignInAnonymousButton extends StatelessWidget {
/// Used in DesktopSignInScreen and MobileSignInScreen
const SignInAnonymousButton({
super.key,
});
@override
Widget build(BuildContext context) {
final isMobile = PlatformExtension.isMobile;
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, signInState) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
..add(
const HistoricalUserEvent.initial(),
),
child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
listenWhen: (previous, current) =>
previous.openedHistoricalUser != current.openedHistoricalUser,
listener: (context, state) async {
await runAppFlowy();
},
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
builder: (context, state) {
final text = state.historicalUsers.isEmpty
? LocaleKeys.signIn_loginStartWithAnonymous.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
final onTap = state.historicalUsers.isEmpty
? () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
}
: () {
final bloc = context.read<HistoricalUserBloc>();
final user = bloc.state.historicalUsers.first;
bloc.add(HistoricalUserEvent.openHistoricalUser(user));
};
// SignInAnonymousButton in mobile
if (isMobile) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
),
onPressed: onTap,
child: Text(LocaleKeys.signIn_loginStartWithAnonymous.tr()),
);
}
// SignInAnonymousButton in desktop
return SizedBox(
height: 48,
child: FlowyButton(
isSelected: true,
disable: signInState.isSubmitting,
text: FlowyText.medium(
text,
textAlign: TextAlign.center,
),
radius: Corners.s6Border,
onTap: onTap,
),
);
},
),
),
);
},
);
}
}

View File

@ -0,0 +1,219 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ThirdPartySignInButtons extends StatelessWidget {
/// Used in DesktopSignInScreen and MobileSignInScreen
const ThirdPartySignInButtons({
super.key,
});
@override
Widget build(BuildContext context) {
final isMobile = PlatformExtension.isMobile;
// Get themeMode from AppearanceSettingsCubit
// When user changes themeMode, it changes the state in AppearanceSettingsCubit, but the themeMode for the MaterialApp won't change, it only got updated(get value from AppearanceSettingsCubit) when user open the app again. Thus, we should get themeMode from AppearanceSettingsCubit rather than MediaQuery.
final isDarkMode =
context.read<AppearanceSettingsCubit>().state.themeMode ==
ThemeMode.dark;
if (isMobile) {
// ThirdPartySignInButtons in mobile
return Column(
children: [
_ThirdPartySignInButton(
isMobile: true,
icon: FlowySvgs.google_mark_xl,
labelText: LocaleKeys.signIn_LogInWithGoogle.tr(),
onPressed: () {
_signInWithGoogle(context);
},
),
const SizedBox(height: 8),
_ThirdPartySignInButton(
isMobile: true,
icon: isDarkMode
? FlowySvgs.github_mark_white_xl
: FlowySvgs.github_mark_black_xl,
labelText: LocaleKeys.signIn_LogInWithGithub.tr(),
onPressed: () {
_signInWithGithub(context);
},
),
const SizedBox(height: 8),
_ThirdPartySignInButton(
isMobile: true,
icon: isDarkMode
? FlowySvgs.discord_mark_white_xl
: FlowySvgs.discord_mark_blurple_xl,
labelText: LocaleKeys.signIn_LogInWithDiscord.tr(),
onPressed: () {
_signInWithDiscord(context);
},
),
],
);
}
// ThirdPartySignInButtons in desktop
return Column(
children: [
_ThirdPartySignInButton(
key: const Key('signInWithGoogleButton'),
isMobile: false,
icon: FlowySvgs.google_mark_xl,
labelText: LocaleKeys.signIn_LogInWithGoogle.tr(),
onPressed: () {
_signInWithGoogle(context);
},
),
const SizedBox(height: 8),
_ThirdPartySignInButton(
isMobile: false,
icon: isDarkMode
? FlowySvgs.github_mark_white_xl
: FlowySvgs.github_mark_black_xl,
labelText: LocaleKeys.signIn_LogInWithGithub.tr(),
onPressed: () {
_signInWithGithub(context);
},
),
const SizedBox(height: 8),
_ThirdPartySignInButton(
isMobile: false,
icon: isDarkMode
? FlowySvgs.discord_mark_white_xl
: FlowySvgs.discord_mark_blurple_xl,
labelText: LocaleKeys.signIn_LogInWithDiscord.tr(),
onPressed: () {
_signInWithDiscord(context);
},
),
],
);
}
}
class _ThirdPartySignInButton extends StatelessWidget {
const _ThirdPartySignInButton({
super.key,
required this.isMobile,
required this.icon,
required this.labelText,
required this.onPressed,
});
final bool isMobile;
final FlowySvgData icon;
final String labelText;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
final buttonSize = MediaQuery.of(context).size;
if (isMobile) {
return SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
icon: Container(
width: buttonSize.width / 5.5,
alignment: Alignment.centerRight,
child: SizedBox(
width: 24,
child: FlowySvg(
icon,
blendMode: null,
),
),
),
label: Container(
padding: const EdgeInsets.only(left: 4),
alignment: Alignment.centerLeft,
child: Text(labelText),
),
onPressed: onPressed,
),
);
}
return SizedBox(
height: 48,
width: double.infinity,
child: OutlinedButton.icon(
// In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left.
icon: Container(
width: buttonSize.width / 8,
alignment: Alignment.centerRight,
child: SizedBox(
// Some icons are not square, so we just use a fixed width here.
width: 24,
child: FlowySvg(
icon,
blendMode: null,
),
),
),
label: Container(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
child: FlowyText(
labelText,
fontSize: 14,
),
),
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(MaterialState.hovered)) {
return style.colorScheme.onSecondaryContainer;
}
return null;
},
),
shape: MaterialStateProperty.all(
const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
),
side: MaterialStateProperty.all(
BorderSide(
color: style.dividerColor,
),
),
),
onPressed: onPressed,
),
);
}
}
void _signInWithGoogle(BuildContext context) {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context.read<SignInBloc>().add(
const SignInEvent.signedInWithOAuth('google'),
);
}
void _signInWithGithub(BuildContext context) {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context.read<SignInBloc>().add(const SignInEvent.signedInWithOAuth('github'));
}
void _signInWithDiscord(BuildContext context) {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('discord'));
}

View File

@ -0,0 +1,2 @@
export 'sign_in_anonymous_button.dart';
export 'third_party_sign_in_buttons.dart';

View File

@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_up_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
@ -23,6 +23,7 @@ class SignUpScreen extends StatelessWidget {
required this.router,
});
static const routeName = '/SignUpScreen';
final AuthRouter router;
@override
@ -46,7 +47,7 @@ class SignUpScreen extends StatelessWidget {
Either<UserProfilePB, FlowyError> result,
) {
result.fold(
(user) => router.pushWelcomeScreen(context, user),
(user) => router.pushWorkspaceStartScreen(context, user),
(error) => showSnapBar(context, error.msg),
);
}
@ -54,8 +55,8 @@ class SignUpScreen extends StatelessWidget {
class SignUpForm extends StatelessWidget {
const SignUpForm({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -89,8 +90,8 @@ class SignUpForm extends StatelessWidget {
class SignUpPrompt extends StatelessWidget {
const SignUpPrompt({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -118,8 +119,8 @@ class SignUpPrompt extends StatelessWidget {
class SignUpButton extends StatelessWidget {
const SignUpButton({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -137,8 +138,8 @@ class SignUpButton extends StatelessWidget {
class PasswordTextField extends StatelessWidget {
const PasswordTextField({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -170,8 +171,8 @@ class PasswordTextField extends StatelessWidget {
class RepeatPasswordTextField extends StatelessWidget {
const RepeatPasswordTextField({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -203,8 +204,8 @@ class RepeatPasswordTextField extends StatelessWidget {
class EmailTextField extends StatelessWidget {
const EmailTextField({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View File

@ -1,41 +1,35 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/launch_configuration.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/language.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../generated/locale_keys.g.dart';
import 'folder/folder_widget.dart';
import 'router.dart';
import 'widgets/background.dart';
class SkipLogInScreen extends StatefulWidget {
final AuthRouter router;
final AuthService authService;
static const routeName = '/SkipLogInScreen';
const SkipLogInScreen({
Key? key,
super.key,
required this.router,
required this.authService,
}) : super(key: key);
});
@override
State<SkipLogInScreen> createState() => _SkipLogInScreenState();
@ -98,9 +92,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
Log.error(error);
},
(user) {
FolderEventGetCurrentWorkspace().send().then((result) {
_openCurrentWorkspace(context, user, result);
});
widget.router.pushHomeScreen(context, user);
},
);
}
@ -114,22 +106,6 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
),
);
}
void _openCurrentWorkspace(
BuildContext context,
UserProfilePB user,
dartz.Either<WorkspaceSettingPB, FlowyError> workspacesOrError,
) {
workspacesOrError.fold(
(workspaceSetting) {
widget.router
.pushHomeScreenWithWorkSpace(context, user, workspaceSetting);
},
(error) {
Log.error(error);
},
);
}
}
class SkipLoginPageFooter extends StatelessWidget {

View File

@ -1,22 +1,34 @@
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/application/splash_bloc.dart';
import 'package:appflowy/user/domain/auth_state.dart';
import 'package:appflowy/user/presentation/helpers/helpers.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../startup/startup.dart';
import '../application/splash_bloc.dart';
import '../domain/auth_state.dart';
import 'router.dart';
// [[diagram: splash screen]]
// 1.get user 2.send UserEventCheckUser
// SplashScreen SplashBlocISplashUser
//
//
//
//
// HomeScreen BlocListener RustSDK
//
// 4. Show HomeScreen or SignIn 3.return AuthState
class SplashScreen extends StatelessWidget {
const SplashScreen({
Key? key,
super.key,
required this.autoRegister,
}) : super(key: key);
});
static const routeName = '/SplashScreen';
final bool autoRegister;
@override
@ -75,15 +87,13 @@ class SplashScreen extends StatelessWidget {
final result = await FolderEventGetCurrentWorkspace().send();
result.fold(
(workspaceSetting) {
getIt<SplashRoute>().pushHomeScreen(
getIt<SplashRouter>().pushHomeScreen(
context,
userProfile,
workspaceSetting,
);
},
(error) {
handleOpenWorkspaceError(context, error);
},
(error) => handleOpenWorkspaceError(context, error),
);
}
},
@ -94,11 +104,14 @@ class SplashScreen extends StatelessWidget {
}
void _handleUnauthenticated(BuildContext context, Unauthenticated result) {
Log.debug(
'_handleUnauthenticated -> Supabase is enabled: $isSupabaseEnabled',
);
// if the env is not configured, we will skip to the 'skip login screen'.
if (isSupabaseEnabled) {
getIt<SplashRoute>().pushSignInScreen(context);
getIt<SplashRouter>().pushSignInScreen(context);
} else {
getIt<SplashRoute>().pushSkipLoginScreen(context);
getIt<SplashRouter>().pushSkipLoginScreen(context);
}
}
@ -111,27 +124,41 @@ class SplashScreen extends StatelessWidget {
}
class Body extends StatelessWidget {
const Body({Key? key}) : super(key: key);
const Body({super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: PlatformExtension.isMobile
? const FlowySvg(
FlowySvgs.flowy_logo_xl,
blendMode: null,
)
: const _DesktopSplashBody(),
);
}
}
class _DesktopSplashBody extends StatelessWidget {
const _DesktopSplashBody();
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Container(
alignment: Alignment.center,
child: SingleChildScrollView(
child: Stack(
alignment: Alignment.center,
children: [
Image(
fit: BoxFit.cover,
width: size.width,
height: size.height,
image:
const AssetImage('assets/images/appflowy_launch_splash.jpg'),
return SingleChildScrollView(
child: Stack(
alignment: Alignment.center,
children: [
Image(
fit: BoxFit.cover,
width: size.width,
height: size.height,
image: const AssetImage(
'assets/images/appflowy_launch_splash.jpg',
),
const CircularProgressIndicator.adaptive(),
],
),
),
const CircularProgressIndicator.adaptive(),
],
),
);
}

View File

@ -10,9 +10,10 @@ import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/workspace_error_bloc.dart';
import '../../application/workspace_error_bloc.dart';
class WorkspaceErrorScreen extends StatelessWidget {
static const routeName = "/WorkspaceErrorScreen";
final FlowyError error;
final UserFolderPB userFolder;
const WorkspaceErrorScreen({

View File

@ -0,0 +1,106 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DesktopWorkspaceStartScreen extends StatelessWidget {
const DesktopWorkspaceStartScreen({super.key, required this.workspaceState});
final WorkspaceState workspaceState;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(60.0),
child: Column(
children: [
_renderBody(workspaceState),
_renderCreateButton(context),
],
),
),
);
}
}
Widget _renderBody(WorkspaceState state) {
final body = state.successOrFailure.fold(
(_) => _renderList(state.workspaces),
(error) => FlowyErrorPage.message(
error.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
);
return body;
}
Widget _renderList(List<WorkspacePB> workspaces) {
return Expanded(
child: StyledListView(
itemBuilder: (BuildContext context, int index) {
final workspace = workspaces[index];
return _WorkspaceItem(
workspace: workspace,
onPressed: (workspace) => _popToWorkspace(context, workspace),
);
},
itemCount: workspaces.length,
),
);
}
class _WorkspaceItem extends StatelessWidget {
final WorkspacePB workspace;
final void Function(WorkspacePB workspace) onPressed;
const _WorkspaceItem({
required this.workspace,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 46,
child: FlowyTextButton(
workspace.name,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fontSize: 14,
onPressed: () => onPressed(workspace),
),
);
}
}
Widget _renderCreateButton(BuildContext context) {
return SizedBox(
width: 200,
height: 40,
child: FlowyTextButton(
LocaleKeys.workspace_create.tr(),
fontSize: 14,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () {
// same method as in mobile
context.read<WorkspaceBloc>().add(
WorkspaceEvent.createWorkspace(
LocaleKeys.workspace_hint.tr(),
"",
),
);
},
),
);
}
// same method as in mobile
void _popToWorkspace(BuildContext context, WorkspacePB workspace) {
context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace));
Navigator.of(context).pop(workspace.id);
}

View File

@ -0,0 +1,144 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// TODO(yijing): needs refactor when multiple workspaces are supported
class MobileWorkspaceStartScreen extends StatefulWidget {
const MobileWorkspaceStartScreen({
super.key,
required this.workspaceState,
});
@override
State<MobileWorkspaceStartScreen> createState() =>
_MobileWorkspaceStartScreenState();
final WorkspaceState workspaceState;
}
class _MobileWorkspaceStartScreenState
extends State<MobileWorkspaceStartScreen> {
WorkspacePB? selectedWorkspace;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
final size = MediaQuery.of(context).size;
const double spacing = 16.0;
final TextEditingController controller = TextEditingController();
final List<DropdownMenuEntry<WorkspacePB>> workspaceEntries =
<DropdownMenuEntry<WorkspacePB>>[];
for (final WorkspacePB workspace in widget.workspaceState.workspaces) {
workspaceEntries.add(
DropdownMenuEntry<WorkspacePB>(
value: workspace,
label: workspace.name,
),
);
}
// render the workspace dropdown menu if success, otherwise render error page
final body = widget.workspaceState.successOrFailure.fold(
(_) {
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 30),
child: Column(
children: [
const Spacer(),
const FlowySvg(
FlowySvgs.flowy_logo_xl,
size: Size.square(64),
blendMode: null,
),
const VSpace(spacing * 2),
Text(
LocaleKeys.workspace_chooseWorkspace.tr(),
style: style.textTheme.displaySmall,
textAlign: TextAlign.center,
),
const VSpace(spacing * 4),
DropdownMenu<WorkspacePB>(
width: size.width - 100,
// TODO(yijing): The following code cause the bad state error, need to fix it
// initialSelection: widget.workspaceState.workspaces.first,
label: const Text('Workspace'),
controller: controller,
dropdownMenuEntries: workspaceEntries,
onSelected: (WorkspacePB? workspace) {
setState(() {
selectedWorkspace = workspace;
});
},
),
const Spacer(),
// TODO(yijing): needs to implement create workspace in the future
// TextButton(
// child: Text(
// LocaleKeys.workspace_create.tr(),
// style: style.textTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// onPressed: () {
// setState(() {
// // same method as in desktop
// context.read<WorkspaceBloc>().add(
// WorkspaceEvent.createWorkspace(
// LocaleKeys.workspace_hint.tr(),
// "",
// ),
// );
// });
// },
// ),
const VSpace(spacing / 2),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
),
onPressed: () {
if (selectedWorkspace == null) {
// If user didn't choose any workspace, pop to the initial workspace(first workspace)
_popToWorkspace(
context,
widget.workspaceState.workspaces.first,
);
return;
}
// pop to the selected workspace
_popToWorkspace(
context,
selectedWorkspace!,
);
},
child: Text(LocaleKeys.signUp_getStartedText.tr()),
),
const VSpace(spacing),
],
),
);
},
(error) {
return FlowyErrorPage.message(
error.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
);
},
);
return Scaffold(
body: body,
);
}
}
// same method as in desktop
void _popToWorkspace(BuildContext context, WorkspacePB workspace) {
context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace));
Navigator.of(context).pop(workspace.id);
}

View File

@ -0,0 +1,38 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart';
import 'package:appflowy/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/application/workspace/workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// For future use
class WorkspaceStartScreen extends StatelessWidget {
final UserProfilePB userProfile;
static const routeName = "/WorkspaceStartScreen";
const WorkspaceStartScreen({
super.key,
required this.userProfile,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<WorkspaceBloc>(param1: userProfile)
..add(const WorkspaceEvent.initial()),
child: BlocBuilder<WorkspaceBloc, WorkspaceState>(
builder: (context, state) {
if (PlatformExtension.isMobile) {
return MobileWorkspaceStartScreen(
workspaceState: state,
);
}
return DesktopWorkspaceStartScreen(
workspaceState: state,
);
},
),
);
}
}

View File

@ -1,491 +0,0 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SignInScreen extends StatelessWidget {
const SignInScreen({
super.key,
required this.router,
});
final AuthRouter router;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<SignInBloc>(),
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail.fold(
() => null,
(result) => _handleSuccessOrFail(result, context),
);
},
builder: (_, __) => Scaffold(
appBar: const PreferredSize(
preferredSize: Size(double.infinity, 60),
child: MoveWindowDetector(),
),
body: SignInForm(router: router),
),
),
);
}
void _handleSuccessOrFail(
Either<UserProfilePB, FlowyError> result,
BuildContext context,
) {
result.fold(
(user) {
if (user.encryptionType == EncryptionTypePB.Symmetric) {
router.pushEncryptionScreen(context, user);
} else {
router.pushHomeScreen(context, user);
}
},
(error) {
handleOpenWorkspaceError(context, error);
},
);
}
}
void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
Log.error(error);
switch (error.code) {
case ErrorCode.WorkspaceDataNotSync:
final userFolder = UserFolderPB.fromBuffer(error.payload);
getIt<AuthRouter>().pushWorkspaceErrorScreen(context, userFolder, error);
break;
case ErrorCode.InvalidEncryptSecret:
showSnapBar(
context,
error.msg,
);
break;
default:
showSnapBar(
context,
error.msg,
onClosed: () {
getIt<AuthService>().signOut();
runAppFlowy();
},
);
}
}
class SignInForm extends StatelessWidget {
const SignInForm({
super.key,
required this.router,
});
final AuthRouter router;
@override
Widget build(BuildContext context) {
final isSubmitting = context.read<SignInBloc>().state.isSubmitting;
const indicatorMinHeight = 4.0;
return Align(
alignment: Alignment.center,
child: AuthFormContainer(
children: [
// Email.
FlowyLogoTitle(
title: LocaleKeys.signIn_loginTitle.tr(),
logoSize: const Size(60, 60),
),
const VSpace(30),
// Email and password. don't support yet.
/*
...[
const EmailTextField(),
const VSpace(5),
const PasswordTextField(),
const VSpace(20),
const LoginButton(),
const VSpace(10),
const VSpace(10),
SignUpPrompt(router: router),
],
*/
const SignInAsGuestButton(),
// third-party sign in.
const VSpace(20),
const OrContinueWith(),
const VSpace(10),
const ThirdPartySignInButtons(),
const VSpace(20),
// loading status
...isSubmitting
? [
const VSpace(indicatorMinHeight),
const LinearProgressIndicator(
value: null,
minHeight: indicatorMinHeight,
),
]
: [
const VSpace(indicatorMinHeight * 2.0)
], // add the same space when there's no loading status.
// ConstrainedBox(
// constraints: const BoxConstraints(maxHeight: 140),
// child: HistoricalUserList(
// didOpenUser: () async {
// await FlowyRunner.run(
// FlowyApp(),
// integrationEnv(),
// );
// },
// ),
// ),
const VSpace(20),
],
),
);
}
}
class SignUpPrompt extends StatelessWidget {
const SignUpPrompt({
Key? key,
required this.router,
}) : super(key: key);
final AuthRouter router;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText.medium(
LocaleKeys.signIn_dontHaveAnAccount.tr(),
color: Theme.of(context).hintColor,
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
),
onPressed: () => router.pushSignUpScreen(context),
child: Text(
LocaleKeys.signUp_buttonText.tr(),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
ForgetPasswordButton(router: router),
],
);
}
}
class LoginButton extends StatelessWidget {
const LoginButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RoundedTextButton(
title: LocaleKeys.signIn_loginButtonText.tr(),
height: 48,
borderRadius: Corners.s10Border,
onPressed: () => context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithUserEmailAndPassword()),
);
}
}
class SignInAsGuestButton extends StatelessWidget {
const SignInAsGuestButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, signInState) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
..add(
const HistoricalUserEvent.initial(),
),
child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
listenWhen: (previous, current) =>
previous.openedHistoricalUser != current.openedHistoricalUser,
listener: (context, state) async {
await runAppFlowy();
},
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
builder: (context, state) {
final text = state.historicalUsers.isEmpty
? LocaleKeys.signIn_loginAsGuestButtonText.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
final onTap = state.historicalUsers.isEmpty
? () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'local');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
}
: () {
final bloc = context.read<HistoricalUserBloc>();
final user = bloc.state.historicalUsers.first;
bloc.add(HistoricalUserEvent.openHistoricalUser(user));
};
return SizedBox(
height: 48,
child: FlowyButton(
isSelected: true,
disable: signInState.isSubmitting,
text: FlowyText.medium(
text,
textAlign: TextAlign.center,
),
radius: Corners.s6Border,
onTap: onTap,
),
);
},
),
),
);
},
);
}
}
class ForgetPasswordButton extends StatelessWidget {
const ForgetPasswordButton({
Key? key,
required this.router,
}) : super(key: key);
final AuthRouter router;
@override
Widget build(BuildContext context) {
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
),
onPressed: () {
throw UnimplementedError();
},
child: Text(
LocaleKeys.signIn_forgotPassword.tr(),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
);
}
}
class PasswordTextField extends StatelessWidget {
const PasswordTextField({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInBloc, SignInState>(
buildWhen: (previous, current) =>
previous.passwordError != current.passwordError,
builder: (context, state) {
return RoundedInputField(
obscureText: true,
obscureIcon: const FlowySvg(FlowySvgs.hide_m),
obscureHideIcon: const FlowySvg(FlowySvgs.show_m),
hintText: LocaleKeys.signIn_passwordHint.tr(),
errorText: context
.read<SignInBloc>()
.state
.passwordError
.fold(() => "", (error) => error),
onChanged: (value) => context
.read<SignInBloc>()
.add(SignInEvent.passwordChanged(value)),
);
},
);
}
}
class EmailTextField extends StatelessWidget {
const EmailTextField({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInBloc, SignInState>(
buildWhen: (previous, current) =>
previous.emailError != current.emailError,
builder: (context, state) {
return RoundedInputField(
hintText: LocaleKeys.signIn_emailHint.tr(),
errorText: context
.read<SignInBloc>()
.state
.emailError
.fold(() => "", (error) => error),
onChanged: (value) =>
context.read<SignInBloc>().add(SignInEvent.emailChanged(value)),
);
},
);
}
}
class OrContinueWith extends StatelessWidget {
const OrContinueWith({super.key});
@override
Widget build(BuildContext context) {
return const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(
child: Divider(
color: Colors.white,
height: 10,
),
),
FlowyText.regular(' Or continue with '),
Flexible(
child: Divider(
color: Colors.white,
height: 10,
),
),
],
);
}
}
class ThirdPartySignInButton extends StatelessWidget {
const ThirdPartySignInButton({
Key? key,
required this.icon,
required this.onPressed,
}) : super(key: key);
final FlowySvgData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
height: 48,
width: 48,
iconPadding: const EdgeInsets.all(8.0),
radius: Corners.s10Border,
onPressed: onPressed,
icon: FlowySvg(
icon,
blendMode: null,
),
);
}
}
class ThirdPartySignInButtons extends StatelessWidget {
final MainAxisAlignment mainAxisAlignment;
const ThirdPartySignInButtons({
this.mainAxisAlignment = MainAxisAlignment.center,
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: mainAxisAlignment,
children: const [
GoogleSignUpButton(),
SizedBox(width: 20),
GitHubSignUpButton(),
SizedBox(width: 20),
DiscordSignUpButton(),
],
);
}
}
class GoogleSignUpButton extends StatelessWidget {
const GoogleSignUpButton({super.key});
@override
Widget build(BuildContext context) {
return ThirdPartySignInButton(
icon: FlowySvgs.google_mark_xl,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context.read<SignInBloc>().add(
const SignInEvent.signedInWithOAuth('google'),
);
},
);
}
}
class GitHubSignUpButton extends StatelessWidget {
const GitHubSignUpButton({super.key});
@override
Widget build(BuildContext context) {
return ThirdPartySignInButton(
icon: FlowySvgs.github_mark_s,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('github'));
},
);
}
}
class DiscordSignUpButton extends StatelessWidget {
const DiscordSignUpButton({super.key});
@override
Widget build(BuildContext context) {
return ThirdPartySignInButton(
icon: FlowySvgs.discord_mark_s,
onPressed: () {
getIt<KeyValueStorage>().set(KVKeys.loginType, 'supabase');
context
.read<SignInBloc>()
.add(const SignInEvent.signedInWithOAuth('discord'));
},
);
}
}

View File

@ -1,115 +0,0 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/workspace/welcome_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class WelcomeScreen extends StatelessWidget {
final UserProfilePB userProfile;
const WelcomeScreen({
Key? key,
required this.userProfile,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<WelcomeBloc>(param1: userProfile)
..add(const WelcomeEvent.initial()),
child: BlocBuilder<WelcomeBloc, WelcomeState>(
builder: (context, state) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(60.0),
child: Column(
children: [
_renderBody(state),
_renderCreateButton(context),
],
),
),
);
},
),
);
}
Widget _renderBody(WelcomeState state) {
final body = state.successOrFailure.fold(
(_) => _renderList(state.workspaces),
(error) => FlowyErrorPage.message(error.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),),
);
return body;
}
Widget _renderCreateButton(BuildContext context) {
return SizedBox(
width: 200,
height: 40,
child: FlowyTextButton(
LocaleKeys.workspace_create.tr(),
fontSize: 14,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () {
context.read<WelcomeBloc>().add(
WelcomeEvent.createWorkspace(
LocaleKeys.workspace_hint.tr(),
"",
),
);
},
),
);
}
Widget _renderList(List<WorkspacePB> workspaces) {
return Expanded(
child: StyledListView(
itemBuilder: (BuildContext context, int index) {
final workspace = workspaces[index];
return WorkspaceItem(
workspace: workspace,
onPressed: (workspace) => _handleOnPress(context, workspace),
);
},
itemCount: workspaces.length,
),
);
}
void _handleOnPress(BuildContext context, WorkspacePB workspace) {
context.read<WelcomeBloc>().add(WelcomeEvent.openWorkspace(workspace));
Navigator.of(context).pop(workspace.id);
}
}
class WorkspaceItem extends StatelessWidget {
final WorkspacePB workspace;
final void Function(WorkspacePB workspace) onPressed;
const WorkspaceItem({
Key? key,
required this.workspace,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 46,
child: FlowyTextButton(
workspace.name,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fontSize: 14,
onPressed: () => onPressed(workspace),
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'dart:math';
import 'package:flutter/material.dart';
class AuthFormContainer extends StatelessWidget {
final List<Widget> children;
const AuthFormContainer({
super.key,
required this.children,
});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return SizedBox(
width: min(size.width, 340),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
}
}

View File

@ -1,40 +1,17 @@
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AuthFormContainer extends StatelessWidget {
final List<Widget> children;
const AuthFormContainer({
Key? key,
required this.children,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return SizedBox(
width: min(size.width, 340),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
}
}
class FlowyLogoTitle extends StatelessWidget {
final String title;
final Size logoSize;
const FlowyLogoTitle({
Key? key,
super.key,
required this.title,
this.logoSize = const Size.square(40),
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View File

@ -102,10 +102,10 @@ class FolderOptionsWidget extends StatelessWidget {
class CreateFolderWidget extends StatefulWidget {
const CreateFolderWidget({
Key? key,
super.key,
required this.onPressedBack,
required this.onPressedCreate,
}) : super(key: key);
});
final VoidCallback onPressedBack;
final Future<void> Function() onPressedCreate;

View File

@ -0,0 +1,3 @@
export 'folder_widget.dart';
export 'flowy_logo_title.dart';
export 'auth_form_container.dart';

View File

@ -0,0 +1,11 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
extension PlatformExtension on Platform {
static bool get isMobile {
if (kIsWeb) {
return false;
}
return Platform.isAndroid || Platform.isIOS;
}
}

View File

@ -1,7 +1,9 @@
import 'dart:async';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/mobile_theme_data.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -281,7 +283,15 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
MaterialState.dragged,
};
return ThemeData(
if (PlatformExtension.isMobile) {
// Mobile version has only one theme(light mode) for now.
// The desktop theme and the mobile theme are independent.
final mobileThemeData = getMobileThemeData();
return mobileThemeData;
}
// Due to Desktop version has multiple themes, it relies on the current theme to build the ThemeData
final desktopThemeData = ThemeData(
brightness: brightness,
dialogBackgroundColor: theme.surface,
textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
@ -372,6 +382,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
)
],
);
return desktopThemeData;
}
TextStyle _getFontStyle({

View File

@ -13,9 +13,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
final UserWorkspaceListener _workspaceListener;
HomeBloc(
UserProfilePB user,
UserProfilePB userProfile,
WorkspaceSettingPB workspaceSetting,
) : _workspaceListener = UserWorkspaceListener(userProfile: user),
) : _workspaceListener = UserWorkspaceListener(userProfile: userProfile),
super(HomeState.initial(workspaceSetting)) {
on<HomeEvent>(
(event, emit) async {

View File

@ -0,0 +1,136 @@
// ThemeData in mobile
import 'package:flutter/material.dart';
ThemeData getMobileThemeData() {
const mobileColorTheme = ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF2DA2F6), //primary 100
onPrimary: Colors.white,
// TODO(yijing): add color later
secondary: Colors.white,
onSecondary: Colors.white,
error: Color(0xffFB006D),
onError: Color(0xffFB006D),
background: Colors.white,
onBackground: Color(0xff2F3030), // title text
outline: Color(0xffBDC0C5), //caption
//Snack bar
surface: Colors.white,
onSurface: Color(0xff2F3030), // title text
);
return ThemeData(
// color
primaryColor: mobileColorTheme.primary, //primary 100
primaryColorLight: const Color(0xFF57B5F8), //primary 80
dividerColor: mobileColorTheme.outline, //caption
scaffoldBackgroundColor: Colors.white,
// button
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
shadowColor: MaterialStateProperty.all(null),
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return const Color(0xFF57B5F8);
}
return mobileColorTheme.primary;
},
),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(
mobileColorTheme.onBackground,
),
backgroundColor: MaterialStateProperty.all(Colors.white),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
side: MaterialStateProperty.all(
BorderSide(
color: mobileColorTheme.outline,
width: 0.5,
),
),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 16),
),
// splash color
overlayColor: MaterialStateProperty.all(
Colors.grey[100],
),
),
),
// text
fontFamily: 'Poppins',
textTheme: const TextTheme(
displayLarge: TextStyle(
color: Color(0xFF57B5F8),
fontSize: 32,
fontFamily: 'Poppins',
fontWeight: FontWeight.w700,
height: 1.20,
letterSpacing: 0.16,
),
displayMedium: TextStyle(
color: Color(0xff2F3030),
fontSize: 32,
fontWeight: FontWeight.w600,
height: 1.20,
letterSpacing: 0.16,
),
// H1 Semi 26
displaySmall: TextStyle(
color: Color(0xFF2F3030),
fontSize: 26,
fontWeight: FontWeight.w600,
height: 1.10,
letterSpacing: 0.13,
),
// body2 14 Regular
bodyMedium: TextStyle(
color: Color(0xFFC5C7CB),
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.20,
letterSpacing: 0.07,
),
// blue text button
labelMedium: TextStyle(
color: Color(0xFF2DA2F6),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.20,
),
),
inputDecorationTheme: InputDecorationTheme(
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
width: 2,
color: Color(0xFF2DA2F6), //primary 100
),
borderRadius: BorderRadius.all(Radius.circular(6)),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: mobileColorTheme.error),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: mobileColorTheme.error),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xffBDC0C5), //caption
),
borderRadius: BorderRadius.all(Radius.circular(6)),
),
),
colorScheme: mobileColorTheme,
);
}

View File

@ -1,3 +1,3 @@
export 'welcome_bloc.dart';
export 'workspace_bloc.dart';
export 'workspace_listener.dart';
export 'workspace_service.dart';

View File

@ -6,12 +6,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
part 'welcome_bloc.freezed.dart';
part 'workspace_bloc.freezed.dart';
class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> {
final UserBackendService userService;
WelcomeBloc({required this.userService}) : super(WelcomeState.initial()) {
on<WelcomeEvent>(
WorkspaceBloc({
required this.userService,
}) : super(WorkspaceState.initial()) {
on<WorkspaceEvent>(
(event, emit) async {
await event.map(
initial: (e) async {
@ -39,7 +41,7 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
);
}
Future<void> _fetchWorkspaces(Emitter<WelcomeState> emit) async {
Future<void> _fetchWorkspaces(Emitter<WorkspaceState> emit) async {
final workspacesOrFailed = await userService.getWorkspaces();
emit(
workspacesOrFailed.fold(
@ -57,7 +59,7 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
Future<void> _openWorkspace(
WorkspacePB workspace,
Emitter<WelcomeState> emit,
Emitter<WorkspaceState> emit,
) async {
final result = await userService.openWorkspace(workspace.id);
emit(
@ -74,7 +76,7 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
Future<void> _createWorkspace(
String name,
String desc,
Emitter<WelcomeState> emit,
Emitter<WorkspaceState> emit,
) async {
final result = await userService.createWorkspace(name, desc);
emit(
@ -92,27 +94,26 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
}
@freezed
class WelcomeEvent with _$WelcomeEvent {
const factory WelcomeEvent.initial() = Initial;
// const factory WelcomeEvent.fetchWorkspaces() = FetchWorkspace;
const factory WelcomeEvent.createWorkspace(String name, String desc) =
class WorkspaceEvent with _$WorkspaceEvent {
const factory WorkspaceEvent.initial() = Initial;
const factory WorkspaceEvent.createWorkspace(String name, String desc) =
CreateWorkspace;
const factory WelcomeEvent.openWorkspace(WorkspacePB workspace) =
const factory WorkspaceEvent.openWorkspace(WorkspacePB workspace) =
OpenWorkspace;
const factory WelcomeEvent.workspacesReveived(
const factory WorkspaceEvent.workspacesReveived(
Either<List<WorkspacePB>, FlowyError> workspacesOrFail,
) = WorkspacesReceived;
}
@freezed
class WelcomeState with _$WelcomeState {
const factory WelcomeState({
class WorkspaceState with _$WorkspaceState {
const factory WorkspaceState({
required bool isLoading,
required List<WorkspacePB> workspaces,
required Either<Unit, FlowyError> successOrFailure,
}) = _WelcomeState;
}) = _WorkspaceState;
factory WelcomeState.initial() => WelcomeState(
factory WorkspaceState.initial() => WorkspaceState(
isLoading: false,
workspaces: List.empty(),
successOrFailure: left(unit),

View File

@ -25,17 +25,21 @@ import '../widgets/edit_panel/edit_panel.dart';
import 'home_layout.dart';
import 'home_stack.dart';
class HomeScreen extends StatefulWidget {
final UserProfilePB user;
class DesktopHomeScreen extends StatefulWidget {
static const routeName = '/DesktopHomeScreen';
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
const HomeScreen(this.user, this.workspaceSetting, {Key? key})
: super(key: key);
const DesktopHomeScreen({
super.key,
required this.userProfile,
required this.workspaceSetting,
});
@override
State<HomeScreen> createState() => _HomeScreenState();
State<DesktopHomeScreen> createState() => _DesktopHomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
class _DesktopHomeScreenState extends State<DesktopHomeScreen> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -43,14 +47,14 @@ class _HomeScreenState extends State<HomeScreen> {
BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
BlocProvider<HomeBloc>(
create: (context) {
return HomeBloc(widget.user, widget.workspaceSetting)
return HomeBloc(widget.userProfile, widget.workspaceSetting)
..add(const HomeEvent.initial());
},
),
BlocProvider<HomeSettingBloc>(
create: (context) {
return HomeSettingBloc(
widget.user,
widget.userProfile,
widget.workspaceSetting,
context.read<AppearanceSettingsCubit>(),
)..add(const HomeSettingEvent.initial());
@ -104,7 +108,7 @@ class _HomeScreenState extends State<HomeScreen> {
final layout = HomeLayout(context, constraints);
final homeStack = HomeStack(
layout: layout,
delegate: HomeScreenStackAdaptor(
delegate: DesktopHomeScreenStackAdaptor(
buildContext: context,
),
);
@ -136,7 +140,7 @@ class _HomeScreenState extends State<HomeScreen> {
}) {
final workspaceSetting = widget.workspaceSetting;
final homeMenu = HomeSideBar(
user: widget.user,
user: widget.userProfile,
workspaceSetting: workspaceSetting,
);
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
@ -255,10 +259,10 @@ class _HomeScreenState extends State<HomeScreen> {
}
}
class HomeScreenStackAdaptor extends HomeStackDelegate {
class DesktopHomeScreenStackAdaptor extends HomeStackDelegate {
final BuildContext buildContext;
HomeScreenStackAdaptor({
DesktopHomeScreenStackAdaptor({
required this.buildContext,
});

View File

@ -30,8 +30,8 @@ class HomeStack extends StatelessWidget {
const HomeStack({
required this.delegate,
required this.layout,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -108,13 +108,13 @@ class FadingIndexedStack extends StatefulWidget {
final Duration duration;
const FadingIndexedStack({
Key? key,
super.key,
required this.index,
required this.children,
this.duration = const Duration(
milliseconds: 250,
),
}) : super(key: key);
});
@override
FadingIndexedStackState createState() => FadingIndexedStackState();
@ -253,7 +253,7 @@ class PageManager {
}
class HomeTopBar extends StatelessWidget {
const HomeTopBar({Key? key, required this.layout}) : super(key: key);
const HomeTopBar({super.key, required this.layout});
final HomeLayout layout;

View File

@ -145,7 +145,7 @@ class NaviItemWidget extends StatelessWidget {
class NaviItemDivider extends StatelessWidget {
final Widget child;
const NaviItemDivider({Key? key, required this.child}) : super(key: key);
const NaviItemDivider({super.key, required this.child});
@override
Widget build(BuildContext context) {

View File

@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:dartz/dartz.dart';
@ -55,9 +55,7 @@ class SettingThirdPartyLogin extends StatelessWidget {
const VSpace(6),
promptMessage,
const VSpace(6),
const ThirdPartySignInButtons(
mainAxisAlignment: MainAxisAlignment.start,
),
const ThirdPartySignInButtons(),
const VSpace(6),
],
);

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z" fill="#5865f2"/></svg>

After

Width:  |  Height:  |  Size: 778 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 775 B

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View File

@ -1,15 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" data-e2e="" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg" width="1" height="1">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M43 24.4313C43 23.084 42.8767 21.7885 42.6475 20.5449H24.3877V27.8945H34.8219C34.3724 30.2695 33.0065 32.2818 30.9532 33.6291V38.3964H37.2189C40.885 35.0886 43 30.2177 43 24.4313Z"
fill="#4285F4"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M24.3872 43.001C29.6219 43.001 34.0107 41.2996 37.2184 38.3978L30.9527 33.6305C29.2165 34.7705 26.9958 35.4441 24.3872 35.4441C19.3375 35.4441 15.0633 32.1018 13.5388 27.6108H7.06152V32.5337C10.2517 38.7433 16.8082 43.001 24.3872 43.001Z"
fill="#34A853"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M13.5395 27.6094C13.1516 26.4695 12.9313 25.2517 12.9313 23.9994C12.9313 22.7472 13.1516 21.5295 13.5395 20.3894V15.4668H7.06217C5.74911 18.0318 5 20.9336 5 23.9994C5 27.0654 5.74911 29.9673 7.06217 32.5323L13.5395 27.6094Z"
fill="#FBBC04"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M24.3872 12.5568C27.2336 12.5568 29.7894 13.5155 31.7987 15.3982L37.3595 9.94866C34.0018 6.88281 29.6131 5 24.3872 5C16.8082 5 10.2517 9.25777 7.06152 15.4674L13.5388 20.39C15.0633 15.8991 19.3375 12.5568 24.3872 12.5568Z"
fill="#EA4335"></path>
</svg>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="5 5 38 38"> <path fill-rule="evenodd" clip-rule="evenodd" d="M43 24.4313C43 23.084 42.8767 21.7885 42.6475 20.5449H24.3877V27.8945H34.8219C34.3724 30.2695 33.0065 32.2818 30.9532 33.6291V38.3964H37.2189C40.885 35.0886 43 30.2177 43 24.4313Z" fill="#4285F4"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M24.3872 43.001C29.6219 43.001 34.0107 41.2996 37.2184 38.3978L30.9527 33.6305C29.2165 34.7705 26.9958 35.4441 24.3872 35.4441C19.3375 35.4441 15.0633 32.1018 13.5388 27.6108H7.06152V32.5337C10.2517 38.7433 16.8082 43.001 24.3872 43.001Z" fill="#34A853"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.5395 27.6094C13.1516 26.4695 12.9313 25.2517 12.9313 23.9994C12.9313 22.7472 13.1516 21.5295 13.5395 20.3894V15.4668H7.06217C5.74911 18.0318 5 20.9336 5 23.9994C5 27.0654 5.74911 29.9673 7.06217 32.5323L13.5395 27.6094Z" fill="#FBBC04"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M24.3872 12.5568C27.2336 12.5568 29.7894 13.5155 31.7987 15.3982L37.3595 9.94866C34.0018 6.88281 29.6131 5 24.3872 5C16.8082 5 10.2517 9.25777 7.06152 15.4674L13.5388 20.39C15.0633 15.8991 19.3375 12.5568 24.3872 12.5568Z" fill="#EA4335"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -32,8 +32,8 @@
"signIn": {
"loginTitle": "Login to @:appName",
"loginButtonText": "Login",
"loginAsGuestButtonText": "Get Started",
"continueAnonymousUser": "Continue in an anonymous session",
"loginStartWithAnonymous": "Start with an anonymous session",
"continueAnonymousUser": "Continue with an anonymous session",
"buttonText": "Sign In",
"forgotPassword": "Forgot Password?",
"emailHint": "Email",
@ -42,9 +42,14 @@
"repeatPasswordEmptyError": "Repeat password can't be empty",
"unmatchedPasswordError": "Repeat password is not the same as password",
"syncPromptMessage": "Syncing the data might take a while. Please don't close this page",
"or": "OR",
"LogInWithGoogle": "Log in with Google",
"LogInWithGithub": "Log in with Github",
"LogInWithDiscord": "Log in with Discord",
"signInWith": "Sign in with:"
},
"workspace": {
"chooseWorkspace": "Choose your workspace",
"create": "Create workspace",
"reset": "Reset workspace",
"resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace",