diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index 50fdebd203..748254d07d 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -68,5 +68,6 @@ class KVKeys { /// The key for saving the last opened workspace id /// /// The workspace id is a string. + @Deprecated('deprecated in version 0.5.5') static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId'; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index ea55a16173..8f8fd99ecb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -3,7 +3,6 @@ import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_d import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 055ae17605..58d5454b4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; @@ -9,6 +10,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:string_validator/string_validator.dart'; class ResizableImage extends StatefulWidget { @@ -95,8 +97,10 @@ class _ResizableImageState extends State { url: widget.src, width: imageWidth - moveDistance, userProfilePB: _userProfilePB, - errorWidgetBuilder: (context, url, error) => - _buildError(context, error), + errorWidgetBuilder: (context, url, error) => _ImageLoadFailedWidget( + width: imageWidth, + error: error, + ), progressIndicatorBuilder: (context, url, progress) => _buildLoading(context), ); @@ -159,31 +163,6 @@ class _ResizableImageState extends State { ); } - Widget _buildError(BuildContext context, Object error) { - return Container( - height: 100, - width: imageWidth, - alignment: Alignment.center, - padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - border: Border.all(), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText(AppFlowyEditorL10n.current.imageLoadFailed), - const VSpace(4), - FlowyText.small( - error.toString(), - textAlign: TextAlign.center, - maxLines: 2, - ), - ], - ), - ); - } - Widget _buildEdgeGesture( BuildContext context, { double? top, @@ -241,3 +220,59 @@ class _ResizableImageState extends State { ); } } + +class _ImageLoadFailedWidget extends StatelessWidget { + const _ImageLoadFailedWidget({ + required this.width, + required this.error, + }); + + final double width; + final Object error; + + @override + Widget build(BuildContext context) { + final error = _getErrorMessage(); + return Container( + height: 140, + width: width, + alignment: Alignment.center, + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all( + color: Colors.grey.withOpacity(0.6), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.broken_image_xl, + size: Size.square(48), + ), + FlowyText( + AppFlowyEditorL10n.current.imageLoadFailed, + ), + const VSpace(6), + if (error != null) + FlowyText( + error, + textAlign: TextAlign.center, + color: Theme.of(context).hintColor.withOpacity(0.6), + fontSize: 10, + maxLines: 2, + ), + ], + ), + ); + } + + String? _getErrorMessage() { + if (error is HttpExceptionWithStatus) { + return 'Error ${(error as HttpExceptionWithStatus).statusCode}'; + } + + return null; + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 932171dcba..f9527635bd 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -1,4 +1,5 @@ import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -7,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -69,6 +71,11 @@ class SignInBloc extends Bloc { ), ); }, + switchLoginType: (type) { + emit( + state.copyWith(loginType: type), + ); + }, ); }, ); @@ -217,6 +224,21 @@ class SignInBloc extends Bloc { } SignInState _stateFromCode(FlowyError error) { + // edge case: 429 is the rate limit error code + // since the error code and error msg are saved in the msg field, + // we need to check if the msg contains 429 + final msg = error.msg; + if (msg.isNotEmpty) { + if (msg.contains('429')) { + return state.copyWith( + isSubmitting: false, + successOrFail: FlowyResult.failure( + FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()), + ), + ); + } + } + switch (error.code) { case ErrorCode.EmailFormatInvalid: return state.copyWith( @@ -230,10 +252,19 @@ class SignInBloc extends Bloc { passwordError: error.msg, emailError: null, ); + case ErrorCode.UserUnauthorized: + return state.copyWith( + isSubmitting: false, + successOrFail: FlowyResult.failure( + FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()), + ), + ); default: return state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.failure(error), + successOrFail: FlowyResult.failure( + FlowyError(msg: LocaleKeys.signIn_generalError.tr()), + ), ); } } @@ -253,6 +284,14 @@ class SignInEvent with _$SignInEvent { const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) = DeepLinkStateChange; const factory SignInEvent.cancel() = _Cancel; + const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType; +} + +// we support sign in directly without sign up, but we want to allow the users to sign up if they want to +// this type is only for the UI to know which form to show +enum LoginType { + signIn, + signUp, } @freezed @@ -264,6 +303,7 @@ class SignInState with _$SignInState { required String? passwordError, required String? emailError, required FlowyResult? successOrFail, + @Default(LoginType.signIn) LoginType loginType, }) = _SignInState; factory SignInState.initial() => const SignInState( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index f0c487cc33..3ecacf0961 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -23,12 +23,12 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { case ErrorCode.HttpError: showSnapBar( context, - error.toString(), + error.msg, ); default: showSnapBar( context, - error.toString(), + error.msg, onClosed: () { getIt().signOut(); runAppFlowy(); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index b53c1dff42..b351ff33d0 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -1,62 +1,79 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart'; +import 'package:appflowy/user/application/sign_in_bloc.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; class DesktopSignInScreen extends StatelessWidget { - const DesktopSignInScreen({super.key, required this.isLoading}); - - final bool isLoading; + const DesktopSignInScreen({ + super.key, + }); @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), + return BlocBuilder( + builder: (context, state) { + 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), + + // const SignInAnonymousButton(), + const SignInWithMagicLinkButtons(), + + // third-party sign in. + const VSpace(20), + + if (isAuthEnabled) ...[ + const _OrDivider(), + const VSpace(10), + const ThirdPartySignInButtons(), + ], + const VSpace(20), + + // anonymous sign in + const SignInAnonymousButtonV2(), + const VSpace(10), + + SwitchSignInSignUpButton( + onTap: () { + final type = state.loginType == LoginType.signIn + ? LoginType.signUp + : LoginType.signIn; + context.read().add( + SignInEvent.switchLoginType(type), + ); + }, + ), + + // loading status + const VSpace(indicatorMinHeight), + state.isSubmitting + ? const LinearProgressIndicator( + minHeight: indicatorMinHeight, + ) + : const VSpace(indicatorMinHeight), + const VSpace(20), + ], ), - const VSpace(30), - - // const SignInAnonymousButton(), - const SignInWithMagicLinkButtons(), - - // third-party sign in. - const VSpace(20), - - if (isAuthEnabled) ...[ - const _OrDivider(), - const VSpace(10), - const ThirdPartySignInButtons(), - ], - const VSpace(20), - - // anonymous sign in - const SignInAnonymousButtonV2(), - - // loading status - const VSpace(indicatorMinHeight), - isLoading - ? const LinearProgressIndicator( - minHeight: indicatorMinHeight, - ) - : const VSpace(indicatorMinHeight), - const VSpace(20), - ], - ), - ), + ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 78ee41480e..c91fe35555 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -2,11 +2,12 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart'; +import 'package:appflowy/user/application/sign_in_bloc.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class MobileSignInScreen extends StatelessWidget { @@ -18,28 +19,43 @@ class MobileSignInScreen extends StatelessWidget { Widget build(BuildContext context) { const double spacing = 16; final colorScheme = Theme.of(context).colorScheme; - return Scaffold( - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 40), - child: Column( - children: [ - const Spacer(flex: 4), - _buildLogo(), - const VSpace(spacing * 2), - _buildWelcomeText(), - _buildAppNameText(colorScheme), - const VSpace(spacing * 2), - const SignInWithMagicLinkButtons(), - const VSpace(spacing), - if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), - const VSpace(spacing), - const SignInAnonymousButtonV2(), - const VSpace(spacing), - _buildSettingsButton(context), - if (!isAuthEnabled) const Spacer(flex: 2), - ], - ), - ), + return BlocBuilder( + builder: (context, state) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 40), + child: Column( + children: [ + const Spacer(flex: 4), + _buildLogo(), + const VSpace(spacing * 2), + _buildWelcomeText(), + _buildAppNameText(colorScheme), + const VSpace(spacing * 2), + const SignInWithMagicLinkButtons(), + const VSpace(spacing), + if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), + const VSpace(spacing), + const SignInAnonymousButtonV2(), + const VSpace(spacing), + SwitchSignInSignUpButton( + onTap: () { + final type = state.loginType == LoginType.signIn + ? LoginType.signUp + : LoginType.signIn; + context.read().add( + SignInEvent.switchLoginType(type), + ); + }, + ), + const VSpace(spacing), + _buildSettingsButton(context), + if (!isAuthEnabled) const Spacer(flex: 2), + ], + ), + ), + ); + }, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index a9daf2b961..63f38db424 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -39,9 +39,7 @@ class SignInScreen extends StatelessWidget { ? const MobileLoadingScreen() : const MobileSignInScreen(); } - return DesktopSignInScreen( - isLoading: isLoading, - ); + return const DesktopSignInScreen(); }, ), ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anou b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anou deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 6bccd9ee80..b78197286c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -37,8 +37,10 @@ class _SignInWithMagicLinkButtonsState SizedBox( height: 48.0, child: FlowyTextField( + autoFocus: false, controller: controller, hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), + keyboardType: TextInputType.emailAddress, onSubmitted: (_) => _sendMagicLink(context, controller.text), ), ), @@ -59,6 +61,9 @@ class _SignInWithMagicLinkButtonsState ); return; } + if (context.read().state.isSubmitting) { + return; + } context.read().add(SignInEvent.signedWithMagicLink(email)); showSnackBarMessage( context, @@ -77,33 +82,41 @@ class _ConfirmButton extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformExtension.isMobile) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - ), - onPressed: onTap, - child: FlowyText( - LocaleKeys.signIn_logInWithMagicLink.tr(), - fontSize: 14, - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: FontWeight.w500, - ), - ); - } else { - return SizedBox( - height: 48, - child: FlowyButton( - isSelected: true, - onTap: onTap, - hoverColor: Theme.of(context).colorScheme.primary, - text: FlowyText.medium( - LocaleKeys.signIn_logInWithMagicLink.tr(), - textAlign: TextAlign.center, - ), - radius: Corners.s6Border, - ), - ); - } + return BlocBuilder( + builder: (context, state) { + final name = switch (state.loginType) { + LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(), + LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(), + }; + if (PlatformExtension.isMobile) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + ), + onPressed: onTap, + child: FlowyText( + name, + fontSize: 14, + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ); + } else { + return SizedBox( + height: 48, + child: FlowyButton( + isSelected: true, + onTap: onTap, + hoverColor: Theme.of(context).colorScheme.primary, + text: FlowyText.medium( + name, + textAlign: TextAlign.center, + ), + radius: Corners.s6Border, + ), + ); + } + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart new file mode 100644 index 0000000000..662c1130fa --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart @@ -0,0 +1,52 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SwitchSignInSignUpButton extends StatelessWidget { + const SwitchSignInSignUpButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText( + switch (state.loginType) { + LoginType.signIn => + LocaleKeys.signIn_dontHaveAnAccount.tr(), + LoginType.signUp => + LocaleKeys.signIn_alreadyHaveAnAccount.tr(), + }, + fontSize: 12, + ), + const HSpace(4), + FlowyText( + switch (state.loginType) { + LoginType.signIn => LocaleKeys.signIn_createAccount.tr(), + LoginType.signUp => LocaleKeys.signIn_logIn.tr(), + }, + color: Colors.blue, + fontSize: 12, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index 8fe8214a36..ce446320a3 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -29,37 +29,53 @@ class ThirdPartySignInButtons extends StatelessWidget { ? MediaQuery.of(context).platformBrightness == Brightness.dark : themeModeFromCubit == ThemeMode.dark; - return Column( - children: [ - _ThirdPartySignInButton( - key: const Key('signInWithGoogleButton'), - icon: FlowySvgs.google_mark_xl, - labelText: LocaleKeys.signIn_LogInWithGoogle.tr(), - onPressed: () { - _signInWithGoogle(context); - }, - ), - const VSpace(8), - _ThirdPartySignInButton( - icon: isDarkMode - ? FlowySvgs.github_mark_white_xl - : FlowySvgs.github_mark_black_xl, - labelText: LocaleKeys.signIn_LogInWithGithub.tr(), - onPressed: () { - _signInWithGithub(context); - }, - ), - const VSpace(8), - _ThirdPartySignInButton( - icon: isDarkMode - ? FlowySvgs.discord_mark_white_xl - : FlowySvgs.discord_mark_blurple_xl, - labelText: LocaleKeys.signIn_LogInWithDiscord.tr(), - onPressed: () { - _signInWithDiscord(context); - }, - ), - ], + return BlocBuilder( + builder: (context, state) { + final (googleText, githubText, discordText) = switch (state.loginType) { + LoginType.signIn => ( + LocaleKeys.signIn_signInWithGoogle.tr(), + LocaleKeys.signIn_signInWithGithub.tr(), + LocaleKeys.signIn_signInWithDiscord.tr() + ), + LoginType.signUp => ( + LocaleKeys.signIn_signUpWithGoogle.tr(), + LocaleKeys.signIn_signUpWithGithub.tr(), + LocaleKeys.signIn_signUpWithDiscord.tr() + ), + }; + return Column( + children: [ + _ThirdPartySignInButton( + key: const Key('signInWithGoogleButton'), + icon: FlowySvgs.google_mark_xl, + labelText: googleText, + onPressed: () { + _signInWithGoogle(context); + }, + ), + const VSpace(8), + _ThirdPartySignInButton( + icon: isDarkMode + ? FlowySvgs.github_mark_white_xl + : FlowySvgs.github_mark_black_xl, + labelText: githubText, + onPressed: () { + _signInWithGithub(context); + }, + ), + const VSpace(8), + _ThirdPartySignInButton( + icon: isDarkMode + ? FlowySvgs.discord_mark_white_xl + : FlowySvgs.discord_mark_blurple_xl, + labelText: discordText, + onPressed: () { + _signInWithDiscord(context); + }, + ), + ], + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 116cbc5637..974e2b5927 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,2 +1,5 @@ +export 'magic_link_sign_in_buttons.dart'; export 'sign_in_anonymous_button.dart'; +export 'sign_in_or_logout_button.dart'; +export 'switch_sign_in_sign_up_button.dart'; export 'third_party_sign_in_buttons.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart index 9cd71b270e..9927ee2457 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart @@ -11,9 +11,14 @@ class AuthFormContainer extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( width: width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index d7c26e1a9a..80855872ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,8 +1,5 @@ -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/shared/feature_flags.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -43,14 +40,7 @@ class UserWorkspaceBloc extends Bloc { userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && FeatureFlag.collaborativeWorkspace.isOn; if (currentWorkspace != null && result.$3 == true) { - final result = await _userService - .openWorkspace(currentWorkspace.workspaceId); - result.onSuccess((s) async { - await getIt().set( - KVKeys.lastOpenedWorkspaceId, - currentWorkspace.workspaceId, - ); - }); + await _userService.openWorkspace(currentWorkspace.workspaceId); } emit( state.copyWith( @@ -176,12 +166,6 @@ class UserWorkspaceBloc extends Bloc { ), (e) => state.currentWorkspace, ); - result.onSuccess((_) async { - await getIt().set( - KVKeys.lastOpenedWorkspaceId, - workspaceId, - ); - }); emit( state.copyWith( currentWorkspace: currentWorkspace, @@ -317,32 +301,22 @@ class UserWorkspaceBloc extends Bloc { bool shouldOpenWorkspace, )> _fetchWorkspaces() async { try { - final lastOpenedWorkspaceId = await getIt().get( - KVKeys.lastOpenedWorkspaceId, - ); final currentWorkspace = await _userService.getCurrentWorkspace().getOrThrow(); final workspaces = await _userService.getWorkspaces().getOrThrow(); if (workspaces.isEmpty) { workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace)); } - UserWorkspacePB? currentWorkspaceInList = workspaces - .firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id); - if (lastOpenedWorkspaceId != null) { - final lastOpenedWorkspace = workspaces - .firstWhereOrNull((e) => e.workspaceId == lastOpenedWorkspaceId); - if (lastOpenedWorkspace != null) { - currentWorkspaceInList = lastOpenedWorkspace; - } - } - currentWorkspaceInList ??= workspaces.firstOrNull; + final currentWorkspaceInList = workspaces + .firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id) ?? + workspaces.firstOrNull; return ( currentWorkspaceInList, workspaces ..sort( (a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp), ), - lastOpenedWorkspaceId != currentWorkspace.id + currentWorkspaceInList?.workspaceId != currentWorkspace.id ); } catch (e) { Log.error('fetch workspace error: $e'); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index b9017e04e6..dfd3e7f215 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -1,6 +1,3 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; @@ -13,6 +10,7 @@ import 'package:appflowy/workspace/application/home/home_service.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; @@ -25,12 +23,13 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_infra_ui/style_widget/container.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sized_context/sized_context.dart'; import 'package:styled_widget/styled_widget.dart'; import '../widgets/edit_panel/edit_panel.dart'; - import 'home_layout.dart'; import 'home_stack.dart'; @@ -115,9 +114,15 @@ class DesktopHomeScreen extends StatelessWidget { }, child: BlocBuilder( buildWhen: (previous, current) => previous != current, - builder: (context, state) => FlowyContainer( - Theme.of(context).colorScheme.surface, - child: _buildBody(context, userProfile, workspaceSetting), + builder: (context, state) => BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), + child: FlowyContainer( + Theme.of(context).colorScheme.surface, + child: _buildBody(context, userProfile, workspaceSetting), + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 261df5b4d9..44fadaeee9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; @@ -21,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -58,75 +58,69 @@ class HomeSideBar extends StatelessWidget { // +-- Public Or Private Section: control the sections of the workspace // | // +-- Trash Section - return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add( - const UserWorkspaceEvent.initial(), - ), - child: BlocBuilder( - // Rebuild the whole sidebar when the current workspace changes - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - builder: (context, state) { - if (state.currentWorkspace == null) { - return const SizedBox.shrink(); - } - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => getIt()), - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - ), + return BlocBuilder( + // Rebuild the whole sidebar when the current workspace changes + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => getIt()), + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, ), + ), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => context.read().add( + TabsEvent.openPlugin( + plugin: state.lastCreatedRootView!.plugin(), + ), + ), + ), + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: _onNotificationAction, + ), + BlocListener( + listener: (context, state) { + final actionType = state.actionResult?.actionType; + + if (actionType == UserWorkspaceActionType.create || + actionType == UserWorkspaceActionType.delete || + actionType == UserWorkspaceActionType.open) { + context.read().add( + SidebarSectionsEvent.reload( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ), + ); + context.read().add( + const FavoriteEvent.fetchFavorites(), + ); + } + }, ), ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => context.read().add( - TabsEvent.openPlugin( - plugin: state.lastCreatedRootView!.plugin(), - ), - ), - ), - BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: _onNotificationAction, - ), - BlocListener( - listener: (context, state) { - final actionType = state.actionResult?.actionType; - - if (actionType == UserWorkspaceActionType.create || - actionType == UserWorkspaceActionType.delete || - actionType == UserWorkspaceActionType.open) { - context.read().add( - SidebarSectionsEvent.reload( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - ), - ); - context.read().add( - const FavoriteEvent.fetchFavorites(), - ); - } - }, - ), - ], - child: _Sidebar(userProfile: userProfile), - ), - ); - }, - ), + child: _Sidebar(userProfile: userProfile), + ), + ); + }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 45c2cd5b05..ce4a8a3a0a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; @@ -26,6 +24,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; typedef ViewItemOnSelected = void Function(ViewPB); @@ -411,19 +410,6 @@ class _SingleInnerViewItemState extends State { behavior: HitTestBehavior.translucent, onTap: () => widget.onSelected(widget.view), onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view), - onDoubleTap: isSelected - ? () { - NavigatorTextFieldDialog( - title: LocaleKeys.disclosureAction_rename.tr(), - autoSelectAllText: true, - value: widget.view.name, - maxLength: 256, - onConfirm: (newValue, _) { - context.read().add(ViewEvent.rename(newValue)); - }, - ).show(context); - } - : null, child: SizedBox( height: widget.height, child: Padding( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 34fa615647..cd4314fc01 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -59,6 +59,7 @@ void showSnackBarMessage( ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + duration: duration, action: !showCancel ? null : SnackBarAction( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index ca7dacefc4..3a3ccffcd3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; @@ -13,7 +14,7 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -// workspaces / ... / view_title +// workspace name / ... / view_title class ViewTitleBar extends StatefulWidget { const ViewTitleBar({ super.key, @@ -58,7 +59,7 @@ class _ViewTitleBarState extends State { final replacement = Row( // refresh the view title bar when the ancestors changed key: ValueKey(ancestors.hashCode), - children: _buildViewTitles(ancestors), + children: _buildViewTitles(context, ancestors), ); return LayoutBuilder( builder: (context, constraints) { @@ -79,13 +80,14 @@ class _ViewTitleBarState extends State { ); } - List _buildViewTitles(List views) { + List _buildViewTitles(BuildContext context, List views) { // if the level is too deep, only show the last two view, the first one view and the root view bool hasAddedEllipsis = false; final children = []; for (var i = 0; i < views.length; i++) { final view = views[i]; + if (i >= 1 && i < views.length - 2) { if (!hasAddedEllipsis) { hasAddedEllipsis = true; @@ -95,8 +97,30 @@ class _ViewTitleBarState extends State { } continue; } - children.add( - FlowyTooltip( + + Widget child; + if (i == 0) { + final currentWorkspace = + context.read().state.currentWorkspace; + final icon = currentWorkspace?.icon ?? ''; + final name = currentWorkspace?.name ?? view.name; + // the first one is the workspace name + child = FlowyTooltip( + message: name, + child: Row( + children: [ + EmojiText( + emoji: icon, + fontSize: 18.0, + ), + const HSpace(2.0), + FlowyText.regular(name), + const HSpace(4.0), + ], + ), + ); + } else { + child = FlowyTooltip( message: view.name, child: _ViewTitle( view: view, @@ -105,8 +129,11 @@ class _ViewTitleBarState extends State { : _ViewTitleBehavior.uneditable, // others are not editable onUpdated: () => setState(() => _reloadAncestors()), ), - ), - ); + ); + } + + children.add(child); + if (i != views.length - 1) { // if not the last one, add a divider children.add(const FlowyText.regular('/')); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index b35898f371..778aee0a74 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -7,19 +9,16 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, duration: const Duration(milliseconds: 8000), - content: PopScope( - canPop: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - return true; - }(), - child: FlowyText.medium( - title, - fontSize: 12, - maxLines: 3, - ), + content: FlowyText( + title, + maxLines: 2, + fontSize: + (Platform.isLinux || Platform.isWindows || Platform.isMacOS) + ? 14 + : 12, ), - backgroundColor: Theme.of(context).colorScheme.background, ), ) .closed diff --git a/frontend/resources/flowy_icons/40x/broken_image.svg b/frontend/resources/flowy_icons/40x/broken_image.svg new file mode 100644 index 0000000000..e22236aba3 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/broken_image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f2d94aa5d8..4e079f5b6f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -42,19 +42,28 @@ "emailHint": "Email", "passwordHint": "Password", "dontHaveAnAccount": "Don't have an account?", + "createAccount": "Create account", "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", + "signInWithGoogle": "Log in with Google", + "signInWithGithub": "Log in with Github", + "signInWithDiscord": "Log in with Discord", + "signUpWithGoogle": "Sign up with Google", + "signUpWithGithub": "Sign up with Github", + "signUpWithDiscord": "Sign up with Discord", "signInWith": "Sign in with:", "signInWithEmail": "Sign in with Email", - "logInWithMagicLink": "Log in with Magic Link", + "signInWithMagicLink": "Log in with Magic Link", + "signUpWithMagicLink": "Sign up with Magic Link", "pleaseInputYourEmail": "Please enter your email address", - "magicLinkSent": "Magic link sent to your email, please check your inbox", - "invalidEmail": "Please enter a valid email address" + "magicLinkSent": "We emailed a magic link. Click the link to log in.", + "invalidEmail": "Please enter a valid email address", + "alreadyHaveAnAccount": "Already have an account?", + "logIn": "Log in", + "generalError": "Something went wrong. Please try again later", + "limitRateError": "For security reasons, you can only request a magic link every 60 seconds" }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -912,7 +921,8 @@ "image": { "copiedToPasteBoard": "The image link has been copied to the clipboard", "addAnImage": "Add an image", - "imageUploadFailed": "Image upload failed" + "imageUploadFailed": "Image upload failed", + "errorCode": "Error code" }, "urlPreview": { "copiedToPasteBoard": "The link has been copied to the clipboard", @@ -1375,7 +1385,7 @@ "upload": "Upload", "chooseImage": "Choose an image", "loading": "Loading", - "imageLoadFailed": "Could not load the image", + "imageLoadFailed": "Image load failed", "divider": "Divider", "table": "Table", "colAddBefore": "Add before",