fix: launch review issues 0.5.5 (#5162)

* fix: remove doubel tap to rename

* fix: keep showing the magic link toast

* feat: display workspace name instead of workspace

* feat: set the keyboard type of magic link textfield to email_address

* feat: support switching sign in and sign up

* fix: magic link ui design

* fix: improve sign in error toast

* fix: improve image load failed
This commit is contained in:
Lucas.Xu
2024-04-22 14:04:55 +08:00
committed by GitHub
parent fd4783e19a
commit 568311a855
23 changed files with 501 additions and 309 deletions

View File

@ -68,5 +68,6 @@ class KVKeys {
/// The key for saving the last opened workspace id /// The key for saving the last opened workspace id
/// ///
/// The workspace id is a string. /// The workspace id is a string.
@Deprecated('deprecated in version 0.5.5')
static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId'; static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';
} }

View File

@ -3,7 +3,6 @@ import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_d
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/sign_in_bloc.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/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/application/prelude.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/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/shared/appflowy_network_image.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
class ResizableImage extends StatefulWidget { class ResizableImage extends StatefulWidget {
@ -95,8 +97,10 @@ class _ResizableImageState extends State<ResizableImage> {
url: widget.src, url: widget.src,
width: imageWidth - moveDistance, width: imageWidth - moveDistance,
userProfilePB: _userProfilePB, userProfilePB: _userProfilePB,
errorWidgetBuilder: (context, url, error) => errorWidgetBuilder: (context, url, error) => _ImageLoadFailedWidget(
_buildError(context, error), width: imageWidth,
error: error,
),
progressIndicatorBuilder: (context, url, progress) => progressIndicatorBuilder: (context, url, progress) =>
_buildLoading(context), _buildLoading(context),
); );
@ -159,31 +163,6 @@ class _ResizableImageState extends State<ResizableImage> {
); );
} }
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( Widget _buildEdgeGesture(
BuildContext context, { BuildContext context, {
double? top, double? top,
@ -241,3 +220,59 @@ class _ResizableImageState extends State<ResizableImage> {
); );
} }
} }
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;
}
}

View File

@ -1,4 +1,5 @@
import 'package:appflowy/env/cloud_env.dart'; 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/startup.dart';
import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';
import 'package:appflowy/user/application/auth/auth_service.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' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -69,6 +71,11 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
), ),
); );
}, },
switchLoginType: (type) {
emit(
state.copyWith(loginType: type),
);
},
); );
}, },
); );
@ -217,6 +224,21 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
} }
SignInState _stateFromCode(FlowyError error) { 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) { switch (error.code) {
case ErrorCode.EmailFormatInvalid: case ErrorCode.EmailFormatInvalid:
return state.copyWith( return state.copyWith(
@ -230,10 +252,19 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
passwordError: error.msg, passwordError: error.msg,
emailError: null, emailError: null,
); );
case ErrorCode.UserUnauthorized:
return state.copyWith(
isSubmitting: false,
successOrFail: FlowyResult.failure(
FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()),
),
);
default: default:
return state.copyWith( return state.copyWith(
isSubmitting: false, 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) = const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) =
DeepLinkStateChange; DeepLinkStateChange;
const factory SignInEvent.cancel() = _Cancel; 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 @freezed
@ -264,6 +303,7 @@ class SignInState with _$SignInState {
required String? passwordError, required String? passwordError,
required String? emailError, required String? emailError,
required FlowyResult<UserProfilePB, FlowyError>? successOrFail, required FlowyResult<UserProfilePB, FlowyError>? successOrFail,
@Default(LoginType.signIn) LoginType loginType,
}) = _SignInState; }) = _SignInState;
factory SignInState.initial() => const SignInState( factory SignInState.initial() => const SignInState(

View File

@ -23,12 +23,12 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
case ErrorCode.HttpError: case ErrorCode.HttpError:
showSnapBar( showSnapBar(
context, context,
error.toString(), error.msg,
); );
default: default:
showSnapBar( showSnapBar(
context, context,
error.toString(), error.msg,
onClosed: () { onClosed: () {
getIt<AuthService>().signOut(); getIt<AuthService>().signOut();
runAppFlowy(); runAppFlowy();

View File

@ -1,62 +1,79 @@
import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.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/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DesktopSignInScreen extends StatelessWidget { class DesktopSignInScreen extends StatelessWidget {
const DesktopSignInScreen({super.key, required this.isLoading}); const DesktopSignInScreen({
super.key,
final bool isLoading; });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const indicatorMinHeight = 4.0; const indicatorMinHeight = 4.0;
return Scaffold( return BlocBuilder<SignInBloc, SignInState>(
appBar: const PreferredSize( builder: (context, state) {
preferredSize: Size(double.infinity, 60), return Scaffold(
child: MoveWindowDetector(), appBar: const PreferredSize(
), preferredSize: Size(double.infinity, 60),
body: Center( child: MoveWindowDetector(),
child: AuthFormContainer( ),
children: [ body: Center(
FlowyLogoTitle( child: AuthFormContainer(
title: LocaleKeys.welcomeText.tr(), children: [
logoSize: const Size(60, 60), 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<SignInBloc>().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),
],
),
),
); );
} }
} }

View File

@ -2,11 +2,12 @@ import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/launch_settings_page.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:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class MobileSignInScreen extends StatelessWidget { class MobileSignInScreen extends StatelessWidget {
@ -18,28 +19,43 @@ class MobileSignInScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const double spacing = 16; const double spacing = 16;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return Scaffold( return BlocBuilder<SignInBloc, SignInState>(
body: Padding( builder: (context, state) {
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 40), return Scaffold(
child: Column( body: Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 40),
const Spacer(flex: 4), child: Column(
_buildLogo(), children: [
const VSpace(spacing * 2), const Spacer(flex: 4),
_buildWelcomeText(), _buildLogo(),
_buildAppNameText(colorScheme), const VSpace(spacing * 2),
const VSpace(spacing * 2), _buildWelcomeText(),
const SignInWithMagicLinkButtons(), _buildAppNameText(colorScheme),
const VSpace(spacing), const VSpace(spacing * 2),
if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), const SignInWithMagicLinkButtons(),
const VSpace(spacing), const VSpace(spacing),
const SignInAnonymousButtonV2(), if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme),
const VSpace(spacing), const VSpace(spacing),
_buildSettingsButton(context), const SignInAnonymousButtonV2(),
if (!isAuthEnabled) const Spacer(flex: 2), const VSpace(spacing),
], SwitchSignInSignUpButton(
), onTap: () {
), final type = state.loginType == LoginType.signIn
? LoginType.signUp
: LoginType.signIn;
context.read<SignInBloc>().add(
SignInEvent.switchLoginType(type),
);
},
),
const VSpace(spacing),
_buildSettingsButton(context),
if (!isAuthEnabled) const Spacer(flex: 2),
],
),
),
);
},
); );
} }

View File

@ -39,9 +39,7 @@ class SignInScreen extends StatelessWidget {
? const MobileLoadingScreen() ? const MobileLoadingScreen()
: const MobileSignInScreen(); : const MobileSignInScreen();
} }
return DesktopSignInScreen( return const DesktopSignInScreen();
isLoading: isLoading,
);
}, },
), ),
); );

View File

@ -37,8 +37,10 @@ class _SignInWithMagicLinkButtonsState
SizedBox( SizedBox(
height: 48.0, height: 48.0,
child: FlowyTextField( child: FlowyTextField(
autoFocus: false,
controller: controller, controller: controller,
hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => _sendMagicLink(context, controller.text), onSubmitted: (_) => _sendMagicLink(context, controller.text),
), ),
), ),
@ -59,6 +61,9 @@ class _SignInWithMagicLinkButtonsState
); );
return; return;
} }
if (context.read<SignInBloc>().state.isSubmitting) {
return;
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email)); context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
showSnackBarMessage( showSnackBarMessage(
context, context,
@ -77,33 +82,41 @@ class _ConfirmButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (PlatformExtension.isMobile) { return BlocBuilder<SignInBloc, SignInState>(
return ElevatedButton( builder: (context, state) {
style: ElevatedButton.styleFrom( final name = switch (state.loginType) {
minimumSize: const Size(double.infinity, 56), LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(),
), LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(),
onPressed: onTap, };
child: FlowyText( if (PlatformExtension.isMobile) {
LocaleKeys.signIn_logInWithMagicLink.tr(), return ElevatedButton(
fontSize: 14, style: ElevatedButton.styleFrom(
color: Theme.of(context).colorScheme.onPrimary, minimumSize: const Size(double.infinity, 56),
fontWeight: FontWeight.w500, ),
), onPressed: onTap,
); child: FlowyText(
} else { name,
return SizedBox( fontSize: 14,
height: 48, color: Theme.of(context).colorScheme.onPrimary,
child: FlowyButton( fontWeight: FontWeight.w500,
isSelected: true, ),
onTap: onTap, );
hoverColor: Theme.of(context).colorScheme.primary, } else {
text: FlowyText.medium( return SizedBox(
LocaleKeys.signIn_logInWithMagicLink.tr(), height: 48,
textAlign: TextAlign.center, child: FlowyButton(
), isSelected: true,
radius: Corners.s6Border, onTap: onTap,
), hoverColor: Theme.of(context).colorScheme.primary,
); text: FlowyText.medium(
} name,
textAlign: TextAlign.center,
),
radius: Corners.s6Border,
),
);
}
},
);
} }
} }

View File

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

View File

@ -29,37 +29,53 @@ class ThirdPartySignInButtons extends StatelessWidget {
? MediaQuery.of(context).platformBrightness == Brightness.dark ? MediaQuery.of(context).platformBrightness == Brightness.dark
: themeModeFromCubit == ThemeMode.dark; : themeModeFromCubit == ThemeMode.dark;
return Column( return BlocBuilder<SignInBloc, SignInState>(
children: [ builder: (context, state) {
_ThirdPartySignInButton( final (googleText, githubText, discordText) = switch (state.loginType) {
key: const Key('signInWithGoogleButton'), LoginType.signIn => (
icon: FlowySvgs.google_mark_xl, LocaleKeys.signIn_signInWithGoogle.tr(),
labelText: LocaleKeys.signIn_LogInWithGoogle.tr(), LocaleKeys.signIn_signInWithGithub.tr(),
onPressed: () { LocaleKeys.signIn_signInWithDiscord.tr()
_signInWithGoogle(context); ),
}, LoginType.signUp => (
), LocaleKeys.signIn_signUpWithGoogle.tr(),
const VSpace(8), LocaleKeys.signIn_signUpWithGithub.tr(),
_ThirdPartySignInButton( LocaleKeys.signIn_signUpWithDiscord.tr()
icon: isDarkMode ),
? FlowySvgs.github_mark_white_xl };
: FlowySvgs.github_mark_black_xl, return Column(
labelText: LocaleKeys.signIn_LogInWithGithub.tr(), children: [
onPressed: () { _ThirdPartySignInButton(
_signInWithGithub(context); key: const Key('signInWithGoogleButton'),
}, icon: FlowySvgs.google_mark_xl,
), labelText: googleText,
const VSpace(8), onPressed: () {
_ThirdPartySignInButton( _signInWithGoogle(context);
icon: isDarkMode },
? FlowySvgs.discord_mark_white_xl ),
: FlowySvgs.discord_mark_blurple_xl, const VSpace(8),
labelText: LocaleKeys.signIn_LogInWithDiscord.tr(), _ThirdPartySignInButton(
onPressed: () { icon: isDarkMode
_signInWithDiscord(context); ? 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);
},
),
],
);
},
); );
} }
} }

View File

@ -1,2 +1,5 @@
export 'magic_link_sign_in_buttons.dart';
export 'sign_in_anonymous_button.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'; export 'third_party_sign_in_buttons.dart';

View File

@ -11,9 +11,14 @@ class AuthFormContainer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: width, width: width,
child: Column( child: ScrollConfiguration(
mainAxisAlignment: MainAxisAlignment.center, behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
children: children, child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
),
), ),
); );
} }

View File

@ -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/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.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_listener.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -43,14 +40,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && userProfile.authenticator == AuthenticatorPB.AppFlowyCloud &&
FeatureFlag.collaborativeWorkspace.isOn; FeatureFlag.collaborativeWorkspace.isOn;
if (currentWorkspace != null && result.$3 == true) { if (currentWorkspace != null && result.$3 == true) {
final result = await _userService await _userService.openWorkspace(currentWorkspace.workspaceId);
.openWorkspace(currentWorkspace.workspaceId);
result.onSuccess((s) async {
await getIt<KeyValueStorage>().set(
KVKeys.lastOpenedWorkspaceId,
currentWorkspace.workspaceId,
);
});
} }
emit( emit(
state.copyWith( state.copyWith(
@ -176,12 +166,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
), ),
(e) => state.currentWorkspace, (e) => state.currentWorkspace,
); );
result.onSuccess((_) async {
await getIt<KeyValueStorage>().set(
KVKeys.lastOpenedWorkspaceId,
workspaceId,
);
});
emit( emit(
state.copyWith( state.copyWith(
currentWorkspace: currentWorkspace, currentWorkspace: currentWorkspace,
@ -317,32 +301,22 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
bool shouldOpenWorkspace, bool shouldOpenWorkspace,
)> _fetchWorkspaces() async { )> _fetchWorkspaces() async {
try { try {
final lastOpenedWorkspaceId = await getIt<KeyValueStorage>().get(
KVKeys.lastOpenedWorkspaceId,
);
final currentWorkspace = final currentWorkspace =
await _userService.getCurrentWorkspace().getOrThrow(); await _userService.getCurrentWorkspace().getOrThrow();
final workspaces = await _userService.getWorkspaces().getOrThrow(); final workspaces = await _userService.getWorkspaces().getOrThrow();
if (workspaces.isEmpty) { if (workspaces.isEmpty) {
workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace)); workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace));
} }
UserWorkspacePB? currentWorkspaceInList = workspaces final currentWorkspaceInList = workspaces
.firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id); .firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id) ??
if (lastOpenedWorkspaceId != null) { workspaces.firstOrNull;
final lastOpenedWorkspace = workspaces
.firstWhereOrNull((e) => e.workspaceId == lastOpenedWorkspaceId);
if (lastOpenedWorkspace != null) {
currentWorkspaceInList = lastOpenedWorkspace;
}
}
currentWorkspaceInList ??= workspaces.firstOrNull;
return ( return (
currentWorkspaceInList, currentWorkspaceInList,
workspaces workspaces
..sort( ..sort(
(a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp), (a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
), ),
lastOpenedWorkspaceId != currentWorkspace.id currentWorkspaceInList?.workspaceId != currentWorkspace.id
); );
} catch (e) { } catch (e) {
Log.error('fetch workspace error: $e'); Log.error('fetch workspace error: $e');

View File

@ -1,6 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/plugins/blank/blank.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.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/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/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_ext.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.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' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:flowy_infra_ui/style_widget/container.dart'; 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:flutter_bloc/flutter_bloc.dart';
import 'package:sized_context/sized_context.dart'; import 'package:sized_context/sized_context.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../widgets/edit_panel/edit_panel.dart'; import '../widgets/edit_panel/edit_panel.dart';
import 'home_layout.dart'; import 'home_layout.dart';
import 'home_stack.dart'; import 'home_stack.dart';
@ -115,9 +114,15 @@ class DesktopHomeScreen extends StatelessWidget {
}, },
child: BlocBuilder<HomeSettingBloc, HomeSettingState>( child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (previous, current) => previous != current, buildWhen: (previous, current) => previous != current,
builder: (context, state) => FlowyContainer( builder: (context, state) => BlocProvider(
Theme.of(context).colorScheme.surface, create: (_) => UserWorkspaceBloc(userProfile: userProfile)
child: _buildBody(context, userProfile, workspaceSetting), ..add(
const UserWorkspaceEvent.initial(),
),
child: FlowyContainer(
Theme.of(context).colorScheme.surface,
child: _buildBody(context, userProfile, workspaceSetting),
),
), ),
), ),
), ),

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.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/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.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; show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page. /// 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 // +-- Public Or Private Section: control the sections of the workspace
// | // |
// +-- Trash Section // +-- Trash Section
return BlocProvider<UserWorkspaceBloc>( return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
create: (_) => UserWorkspaceBloc(userProfile: userProfile) // Rebuild the whole sidebar when the current workspace changes
..add( buildWhen: (previous, current) =>
const UserWorkspaceEvent.initial(), previous.currentWorkspace?.workspaceId !=
), current.currentWorkspace?.workspaceId,
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>( builder: (context, state) {
// Rebuild the whole sidebar when the current workspace changes if (state.currentWorkspace == null) {
buildWhen: (previous, current) => return const SizedBox.shrink();
previous.currentWorkspace?.workspaceId != }
current.currentWorkspace?.workspaceId, return MultiBlocProvider(
builder: (context, state) { providers: [
if (state.currentWorkspace == null) { BlocProvider(create: (_) => getIt<ActionNavigationBloc>()),
return const SizedBox.shrink(); BlocProvider(
} create: (_) => SidebarSectionsBloc()
return MultiBlocProvider( ..add(
providers: [ SidebarSectionsEvent.initial(
BlocProvider(create: (_) => getIt<ActionNavigationBloc>()), userProfile,
BlocProvider( state.currentWorkspace?.workspaceId ??
create: (_) => SidebarSectionsBloc() workspaceSetting.workspaceId,
..add(
SidebarSectionsEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
), ),
),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) => context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: state.lastCreatedRootView!.plugin(),
),
),
),
BlocListener<ActionNavigationBloc, ActionNavigationState>(
listenWhen: (_, curr) => curr.action != null,
listener: _onNotificationAction,
),
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
final actionType = state.actionResult?.actionType;
if (actionType == UserWorkspaceActionType.create ||
actionType == UserWorkspaceActionType.delete ||
actionType == UserWorkspaceActionType.open) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.reload(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
context.read<FavoriteBloc>().add(
const FavoriteEvent.fetchFavorites(),
);
}
},
), ),
], ],
child: MultiBlocListener( child: _Sidebar(userProfile: userProfile),
listeners: [ ),
BlocListener<SidebarSectionsBloc, SidebarSectionsState>( );
listenWhen: (p, c) => },
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) => context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: state.lastCreatedRootView!.plugin(),
),
),
),
BlocListener<ActionNavigationBloc, ActionNavigationState>(
listenWhen: (_, curr) => curr.action != null,
listener: _onNotificationAction,
),
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
final actionType = state.actionResult?.actionType;
if (actionType == UserWorkspaceActionType.create ||
actionType == UserWorkspaceActionType.delete ||
actionType == UserWorkspaceActionType.open) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.reload(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
context.read<FavoriteBloc>().add(
const FavoriteEvent.fetchFavorites(),
);
}
},
),
],
child: _Sidebar(userProfile: userProfile),
),
);
},
),
); );
} }

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.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/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
typedef ViewItemOnSelected = void Function(ViewPB); typedef ViewItemOnSelected = void Function(ViewPB);
@ -411,19 +410,6 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => widget.onSelected(widget.view), onTap: () => widget.onSelected(widget.view),
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(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<ViewBloc>().add(ViewEvent.rename(newValue));
},
).show(context);
}
: null,
child: SizedBox( child: SizedBox(
height: widget.height, height: widget.height,
child: Padding( child: Padding(

View File

@ -59,6 +59,7 @@ void showSnackBarMessage(
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
backgroundColor: Theme.of(context).colorScheme.surfaceVariant, backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
duration: duration,
action: !showCancel action: !showCancel
? null ? null
: SnackBarAction( : SnackBarAction(

View File

@ -1,6 +1,7 @@
import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.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/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_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
// workspaces / ... / view_title // workspace name / ... / view_title
class ViewTitleBar extends StatefulWidget { class ViewTitleBar extends StatefulWidget {
const ViewTitleBar({ const ViewTitleBar({
super.key, super.key,
@ -58,7 +59,7 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
final replacement = Row( final replacement = Row(
// refresh the view title bar when the ancestors changed // refresh the view title bar when the ancestors changed
key: ValueKey(ancestors.hashCode), key: ValueKey(ancestors.hashCode),
children: _buildViewTitles(ancestors), children: _buildViewTitles(context, ancestors),
); );
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@ -79,13 +80,14 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
); );
} }
List<Widget> _buildViewTitles(List<ViewPB> views) { List<Widget> _buildViewTitles(BuildContext context, List<ViewPB> views) {
// if the level is too deep, only show the last two view, the first one view and the root view // if the level is too deep, only show the last two view, the first one view and the root view
bool hasAddedEllipsis = false; bool hasAddedEllipsis = false;
final children = <Widget>[]; final children = <Widget>[];
for (var i = 0; i < views.length; i++) { for (var i = 0; i < views.length; i++) {
final view = views[i]; final view = views[i];
if (i >= 1 && i < views.length - 2) { if (i >= 1 && i < views.length - 2) {
if (!hasAddedEllipsis) { if (!hasAddedEllipsis) {
hasAddedEllipsis = true; hasAddedEllipsis = true;
@ -95,8 +97,30 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
} }
continue; continue;
} }
children.add(
FlowyTooltip( Widget child;
if (i == 0) {
final currentWorkspace =
context.read<UserWorkspaceBloc>().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, message: view.name,
child: _ViewTitle( child: _ViewTitle(
view: view, view: view,
@ -105,8 +129,11 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
: _ViewTitleBehavior.uneditable, // others are not editable : _ViewTitleBehavior.uneditable, // others are not editable
onUpdated: () => setState(() => _reloadAncestors()), onUpdated: () => setState(() => _reloadAncestors()),
), ),
), );
); }
children.add(child);
if (i != views.length - 1) { if (i != views.length - 1) {
// if not the last one, add a divider // if not the last one, add a divider
children.add(const FlowyText.regular('/')); children.add(const FlowyText.regular('/'));

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,19 +9,16 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar( .showSnackBar(
SnackBar( SnackBar(
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
duration: const Duration(milliseconds: 8000), duration: const Duration(milliseconds: 8000),
content: PopScope( content: FlowyText(
canPop: () { title,
ScaffoldMessenger.of(context).removeCurrentSnackBar(); maxLines: 2,
return true; fontSize:
}(), (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
child: FlowyText.medium( ? 14
title, : 12,
fontSize: 12,
maxLines: 3,
),
), ),
backgroundColor: Theme.of(context).colorScheme.background,
), ),
) )
.closed .closed

View File

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="40" height="40"><path d="M70.936 57.193c4.38 0 7.943-3.564 7.943-7.945s-3.563-7.945-7.943-7.945c-4.381 0-7.945 3.564-7.945 7.945s3.564 7.945 7.945 7.945zm0-11.89c2.175 0 3.943 1.77 3.943 3.945s-1.769 3.945-3.943 3.945c-2.176 0-3.945-1.77-3.945-3.945s1.769-3.945 3.945-3.945zm40.646 15.56a1.997 1.997 0 0 0-2.216.591l-8.671 10.305-.056.063-18.456 21.935a2 2 0 0 0 1.53 3.288h27.184a2 2 0 0 0 2-2V62.742a2.003 2.003 0 0 0-1.315-1.879zm-2.686 32.182h-18.72l-1.171-1.172 13.293-15.807 6.599 6.598v10.381zm0-16.037-4.014-4.014 4.014-4.77v8.784z" fill="#000000" class="color000 svgShape"></path><path d="M110.896 30.955H17.104a2 2 0 0 0-2 2v62.09a2 2 0 0 0 2 2H67.94c.59 0 1.15-.261 1.53-.712l42.957-51.045c.304-.36.47-.816.47-1.288V32.955a2.002 2.002 0 0 0-2.001-2zm-43.887 62.09H19.104V76.523l24.447-24.447 25.338 25.338 6.122 6.122-8.002 9.509zm10.587-12.58L73.131 76l14.551-14.552 3.213 3.214-13.299 15.803zm31.3-37.194L93.479 61.59l-4.384-4.385a2 2 0 0 0-2.828 0L70.303 73.172 44.965 47.834c-.391-.391-.902-.586-1.414-.586s-1.023.195-1.414.586L19.104 70.867V34.955h89.793v8.316z" fill="#000000" class="color000 svgShape"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -42,19 +42,28 @@
"emailHint": "Email", "emailHint": "Email",
"passwordHint": "Password", "passwordHint": "Password",
"dontHaveAnAccount": "Don't have an account?", "dontHaveAnAccount": "Don't have an account?",
"createAccount": "Create account",
"repeatPasswordEmptyError": "Repeat password can't be empty", "repeatPasswordEmptyError": "Repeat password can't be empty",
"unmatchedPasswordError": "Repeat password is not the same as password", "unmatchedPasswordError": "Repeat password is not the same as password",
"syncPromptMessage": "Syncing the data might take a while. Please don't close this page", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page",
"or": "OR", "or": "OR",
"LogInWithGoogle": "Log in with Google", "signInWithGoogle": "Log in with Google",
"LogInWithGithub": "Log in with Github", "signInWithGithub": "Log in with Github",
"LogInWithDiscord": "Log in with Discord", "signInWithDiscord": "Log in with Discord",
"signUpWithGoogle": "Sign up with Google",
"signUpWithGithub": "Sign up with Github",
"signUpWithDiscord": "Sign up with Discord",
"signInWith": "Sign in with:", "signInWith": "Sign in with:",
"signInWithEmail": "Sign in with Email", "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", "pleaseInputYourEmail": "Please enter your email address",
"magicLinkSent": "Magic link sent to your email, please check your inbox", "magicLinkSent": "We emailed a magic link. Click the link to log in.",
"invalidEmail": "Please enter a valid email address" "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": { "workspace": {
"chooseWorkspace": "Choose your workspace", "chooseWorkspace": "Choose your workspace",
@ -912,7 +921,8 @@
"image": { "image": {
"copiedToPasteBoard": "The image link has been copied to the clipboard", "copiedToPasteBoard": "The image link has been copied to the clipboard",
"addAnImage": "Add an image", "addAnImage": "Add an image",
"imageUploadFailed": "Image upload failed" "imageUploadFailed": "Image upload failed",
"errorCode": "Error code"
}, },
"urlPreview": { "urlPreview": {
"copiedToPasteBoard": "The link has been copied to the clipboard", "copiedToPasteBoard": "The link has been copied to the clipboard",
@ -1375,7 +1385,7 @@
"upload": "Upload", "upload": "Upload",
"chooseImage": "Choose an image", "chooseImage": "Choose an image",
"loading": "Loading", "loading": "Loading",
"imageLoadFailed": "Could not load the image", "imageLoadFailed": "Image load failed",
"divider": "Divider", "divider": "Divider",
"table": "Table", "table": "Table",
"colAddBefore": "Add before", "colAddBefore": "Add before",