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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 workspace id is a string.
@Deprecated('deprecated in version 0.5.5')
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/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';

View File

@ -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<ResizableImage> {
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<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(
BuildContext context, {
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/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<SignInEvent, SignInState> {
),
);
},
switchLoginType: (type) {
emit(
state.copyWith(loginType: type),
);
},
);
},
);
@ -217,6 +224,21 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
}
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<SignInEvent, SignInState> {
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<UserProfilePB, FlowyError>? successOrFail,
@Default(LoginType.signIn) LoginType loginType,
}) = _SignInState;
factory SignInState.initial() => const SignInState(

View File

@ -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<AuthService>().signOut();
runAppFlowy();

View File

@ -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<SignInBloc, SignInState>(
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<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/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<SignInBloc, SignInState>(
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<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 MobileSignInScreen();
}
return DesktopSignInScreen(
isLoading: isLoading,
);
return const DesktopSignInScreen();
},
),
);

View File

@ -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<SignInBloc>().state.isSubmitting) {
return;
}
context.read<SignInBloc>().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<SignInBloc, SignInState>(
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,
),
);
}
},
);
}
}

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

View File

@ -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';

View File

@ -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,
),
),
),
);
}

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/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<UserWorkspaceEvent, UserWorkspaceState> {
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<KeyValueStorage>().set(
KVKeys.lastOpenedWorkspaceId,
currentWorkspace.workspaceId,
);
});
await _userService.openWorkspace(currentWorkspace.workspaceId);
}
emit(
state.copyWith(
@ -176,12 +166,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
),
(e) => state.currentWorkspace,
);
result.onSuccess((_) async {
await getIt<KeyValueStorage>().set(
KVKeys.lastOpenedWorkspaceId,
workspaceId,
);
});
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
@ -317,32 +301,22 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
bool shouldOpenWorkspace,
)> _fetchWorkspaces() async {
try {
final lastOpenedWorkspaceId = await getIt<KeyValueStorage>().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');

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/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<HomeSettingBloc, HomeSettingState>(
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),
),
),
),
),

View File

@ -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<UserWorkspaceBloc>(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
// 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<ActionNavigationBloc>()),
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
// 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<ActionNavigationBloc>()),
BlocProvider(
create: (_) => SidebarSectionsBloc()
..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(
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),
),
);
},
),
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/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<SingleInnerViewItem> {
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<ViewBloc>().add(ViewEvent.rename(newValue));
},
).show(context);
}
: null,
child: SizedBox(
height: widget.height,
child: Padding(

View File

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

View File

@ -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<ViewTitleBar> {
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<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
bool hasAddedEllipsis = false;
final children = <Widget>[];
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<ViewTitleBar> {
}
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,
child: _ViewTitle(
view: view,
@ -105,8 +129,11 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
: _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('/'));

View File

@ -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

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",
"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",