From ac4b82ed7431d66fe810749307fd9d89736fddcc Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 19 Mar 2024 12:30:42 +0100 Subject: [PATCH 01/16] feat: settings account view rework --- .../appearance/desktop_appearance.dart | 4 +- .../appearance/mobile_appearance.dart | 4 +- .../settings/settings_dialog_bloc.dart | 4 +- .../menu/sidebar/folder/personal_folder.dart | 3 + .../settings/pages/settings_account_view.dart | 461 ++++++++++++++++++ .../settings/settings_dialog.dart | 122 ++--- .../shared/settings_alert_dialog.dart | 161 ++++++ .../settings/shared/settings_category.dart | 40 ++ .../shared/settings_category_spacer.dart | 16 + .../settings/shared/settings_header.dart | 40 ++ .../settings/shared/settings_input_field.dart | 163 +++++++ .../settings/shared/settings_subcategory.dart | 42 ++ .../shared/single_setting_action.dart | 76 +++ .../widgets/setting_third_party_login.dart | 15 +- .../settings/widgets/settings_menu.dart | 175 ++++--- .../widgets/settings_menu_element.dart | 26 +- .../more_view_actions/more_view_actions.dart | 2 +- .../presentation/widgets/user_avatar.dart | 53 +- .../lib/colorscheme/colorscheme.dart | 8 +- .../lib/colorscheme/dandelion.dart | 2 + .../lib/colorscheme/default_colorscheme.dart | 4 +- .../flowy_infra/lib/colorscheme/lavender.dart | 2 + .../flowy_infra/lib/colorscheme/lemonade.dart | 2 + .../flowy_infra/lib/theme_extension.dart | 9 + .../lib/style_widget/button.dart | 85 +++- .../lib/style_widget/text_field.dart | 14 +- .../flowy_icons/24x/settings_account.svg | 3 + .../flowy_icons/24x/settings_data.svg | 8 + .../flowy_icons/24x/settings_members.svg | 8 + .../24x/settings_notifications.svg | 8 + .../flowy_icons/24x/settings_plan.svg | 8 + .../flowy_icons/24x/settings_shortcuts.svg | 8 + .../flowy_icons/24x/settings_sync.svg | 8 + .../flowy_icons/24x/settings_workplace.svg | 8 + frontend/resources/translations/en.json | 32 +- 35 files changed, 1399 insertions(+), 225 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart create mode 100644 frontend/resources/flowy_icons/24x/settings_account.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_data.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_members.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_notifications.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_plan.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_shortcuts.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_sync.svg create mode 100644 frontend/resources/flowy_icons/24x/settings_workplace.svg diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 6996432fc773..8925fc9bdaa4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @override @@ -119,6 +120,7 @@ class DesktopAppearance extends BaseAppearance { tint8: theme.tint8, tint9: theme.tint9, textColor: theme.text, + secondaryTextColor: theme.secondaryText, greyHover: theme.hoverBG1, greySelect: theme.bg3, lightGreyHover: theme.hoverBG3, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 6057ddc8d22e..cb8134ad4c27 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; + // ThemeData in mobile import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { static const _primaryColor = Color(0xFF00BCF0); //primary 100 @@ -262,6 +263,7 @@ class MobileAppearance extends BaseAppearance { tint8: theme.tint8, tint9: theme.tint9, textColor: theme.text, + secondaryTextColor: theme.secondaryText, greyHover: theme.hoverBG1, greySelect: theme.bg3, lightGreyHover: theme.hoverBG3, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 2d73d6ebe7ec..382b3441e7f8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -9,6 +9,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_dialog_bloc.freezed.dart'; enum SettingsPage { + account, + // OLD appearance, language, files, @@ -88,6 +90,6 @@ class SettingsDialogState with _$SettingsDialogState { SettingsDialogState( userProfile: userProfile, successOrFailure: FlowyResult.success(null), - page: SettingsPage.appearance, + page: SettingsPage.account, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart index ec86203599cc..84471e71cf1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart @@ -10,6 +10,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_di import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -108,6 +109,8 @@ class _PersonalFolderHeaderState extends State { ), padding: const EdgeInsets.all(textPadding), fillColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + fontHoverColor: Theme.of(context).colorScheme.onPrimary, onPressed: widget.onPressed, ), if (onHover) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart new file mode 100644 index 000000000000..e8ac3889b9d6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -0,0 +1,461 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +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/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsAccountView extends StatefulWidget { + const SettingsAccountView({ + super.key, + required this.userProfile, + required this.didLogin, + required this.didLogout, + }); + + final UserProfilePB userProfile; + + // Called when the user signs in from the setting dialog + final VoidCallback didLogin; + + // Called when the user logout in the setting dialog + final VoidCallback didLogout; + + @override + State createState() => _SettingsAccountViewState(); +} + +class _SettingsAccountViewState extends State { + late String userName = widget.userProfile.name; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt(param1: widget.userProfile) + ..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsHeader( + title: LocaleKeys.settings_account_title.tr(), + description: LocaleKeys.settings_account_description.tr(), + ), + SettingsCategory( + title: LocaleKeys.settings_account_general_title.tr(), + children: [ + _UserProfileSetting( + name: userName, + iconUrl: state.userProfile.iconUrl, + onSave: (newName) { + // Pseudo change the name to update the UI before the backend + // processes the request. This is to give the user a sense of + // immediate feedback, and avoid UI flickering. + setState(() => userName = newName); + context + .read() + .add(SettingsUserEvent.updateUserName(newName)); + }, + ), + ], + ), + // Only show change email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != + AuthenticatorPB.Local) ...[ + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_account_email_title.tr(), + children: [ + SingleSettingAction( + label: state.userProfile.email, + buttonLabel: LocaleKeys + .settings_account_email_actions_change + .tr(), + onPressed: () {}, + ), + ], + ), + ], + + /// TODO: Uncomment and finish implementation when we have the feature + // const SettingsCategorySpacer(), + // SettingsCategory( + // title: 'Account & security', + // children: [ + // SingleSettingAction( + // label: '**********', + // buttonLabel: 'Change password', + // onPressed: () {}, + // ), + // SingleSettingAction( + // label: '2-step authentication', + // buttonLabel: 'Enable 2FA', + // onPressed: () {}, + // ), + // ], + // ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_account_keys_title.tr(), + children: [ + SettingsInputField( + label: LocaleKeys.settings_account_keys_openAILabel.tr(), + tooltip: + LocaleKeys.settings_account_keys_openAITooltip.tr(), + value: state.userProfile.openaiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(key)), + ), + ], + ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_account_login_title.tr(), + children: [ + if (state.userProfile.authenticator == + AuthenticatorPB.Local) ...[ + _SignInButton( + userProfile: state.userProfile, + didLogin: widget.didLogin, + ), + ] else ...[ + _SignOutButton( + userProfile: state.userProfile, + didLogout: widget.didLogout, + ), + ], + ], + ), + + /// TODO: Uncomment and finish implementation when we have the feature + // const SettingsCategorySpacer(), + // SettingsSubcategory( + // title: 'Delete account', + // children: [ + // SingleSettingAction( + // label: + // 'Permanently delete your account and remove access from all teamspaces.', + // labelMaxLines: 4, + // onPressed: () {}, + // buttonLabel: 'Delete my account', + // isDangerous: true, + // fontSize: 12, + // ), + // ], + // ), + ], + ), + ); + }, + ), + ); + } +} + +class _SignInButton extends StatelessWidget { + const _SignInButton({ + required this.userProfile, + required this.didLogin, + }); + + final UserProfilePB userProfile; + final VoidCallback didLogin; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 48, + child: FlowyTextButton( + LocaleKeys.settings_account_login_loginLabel.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () { + SettingsAlertDialog( + title: LocaleKeys.settings_account_login_loginLabel.tr(), + children: [ + SettingThirdPartyLogin( + didLogin: didLogin, + ), + ], + ).show(context); + }, + ), + ), + ], + ); + } +} + +class _SignOutButton extends StatelessWidget { + const _SignOutButton({ + required this.userProfile, + required this.didLogout, + }); + + final UserProfilePB userProfile; + final VoidCallback didLogout; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 48, + child: FlowyTextButton( + LocaleKeys.settings_account_login_logoutLabel.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () { + SettingsAlertDialog( + title: LocaleKeys.settings_account_login_logoutLabel, + subtitle: switch (userProfile.encryptionType) { + EncryptionTypePB.Symmetric => + LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(), + _ => LocaleKeys.settings_menu_logoutPrompt.tr(), + }, + confirm: () async { + await getIt().signOut(); + didLogout(); + }, + ).show(context); + }, + ), + ), + ], + ); + } +} + +class _UserProfileSetting extends StatefulWidget { + const _UserProfileSetting({ + required this.name, + required this.iconUrl, + this.onSave, + }); + + final String name; + final String iconUrl; + final void Function(String)? onSave; + + @override + State<_UserProfileSetting> createState() => _UserProfileSettingState(); +} + +class _UserProfileSettingState extends State<_UserProfileSetting> { + late final FocusNode focusNode; + bool isEditing = false; + bool isHoveringProfileImage = false; + + @override + void initState() { + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + ); + super.initState(); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showIconPickerDialog(context), + child: FlowyHover( + resetHoverOnRebuild: false, + onHover: (isHovering) => + setState(() => isHoveringProfileImage = isHovering), + style: HoverStyle( + hoverColor: Colors.transparent, + borderRadius: BorderRadius.circular(100), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + UserAvatar( + iconUrl: widget.iconUrl, + name: widget.name, + isLarge: true, + isHovering: isHoveringProfileImage, + ), + const VSpace(4), + FlowyText.regular( + LocaleKeys.settings_account_general_changeProfilePicture + .tr(), + color: AFThemeExtension.of(context).textColor, + ), + ], + ), + ), + ), + if (widget.iconUrl.isNotEmpty) + Positioned( + right: 0, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const SettingsUserEvent.removeUserIcon()), + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: FlowyHover( + resetHoverOnRebuild: false, + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(24)), + hoverColor: Color(0xFF005483), + ), + builder: (_, isHovering) => Padding( + padding: const EdgeInsets.all(4), + child: FlowySvg( + FlowySvgs.close_s, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), + ), + ), + ], + ), + const HSpace(16), + if (!isEditing) ...[ + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.medium( + widget.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ], + ), + ), + ] else ...[ + Flexible( + child: SettingsInputField( + value: widget.name, + focusNode: focusNode..requestFocus(), + onSave: (val) { + widget.onSave?.call(val); + setState(() => isEditing = false); + }, + onCancel: () => setState(() => isEditing = false), + ), + ), + ], + ], + ); + } + + Future _showIconPickerDialog(BuildContext context) { + return showDialog( + context: context, + builder: (dialogContext) => SimpleDialog( + title: FlowyText.medium( + LocaleKeys.settings_user_selectAnIcon.tr(), + fontSize: FontSizes.s16, + ), + children: [ + Container( + height: 380, + width: 360, + margin: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl: emoji)); + Navigator.of(dialogContext).pop(); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 484212a011fb..dbc7654445cc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,26 +1,13 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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 'widgets/setting_cloud.dart'; - -const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12); -const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0); - class SettingsDialog extends StatelessWidget { SettingsDialog( this.user, { @@ -41,47 +28,28 @@ class SettingsDialog extends StatelessWidget { ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => FlowyDialog( - title: Padding( - padding: _dialogHorizontalPadding + _contentInsetPadding, - child: FlowyText( - LocaleKeys.settings_title.tr(), - fontSize: 20, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.tertiary, - ), - ), child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, - body: Padding( - padding: _dialogHorizontalPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - changeSelectedPage: (index) { - context - .read() - .add(SettingsDialogEvent.setSelectedPage(index)); - }, - currentPage: - context.read().state.page, - ), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedPage: (index) => context + .read() + .add(SettingsDialogEvent.setSelectedPage(index)), + currentPage: state.page, ), - VerticalDivider( - color: Theme.of(context).dividerColor, + ), + Expanded( + child: getSettingsView( + state.page, + state.userProfile, ), - const SizedBox(width: 10), - Expanded( - child: getSettingsView( - context.read().state.page, - context.read().state.userProfile, - ), - ), - ], - ), + ), + ], ), ), ), @@ -92,31 +60,35 @@ class SettingsDialog extends StatelessWidget { Widget getSettingsView(SettingsPage page, UserProfilePB user) { switch (page) { - case SettingsPage.appearance: - return const SettingsAppearanceView(); - case SettingsPage.language: - return const SettingsLanguageView(); - case SettingsPage.files: - return const SettingsFileSystemView(); - case SettingsPage.user: - return SettingsUserView( - user, - didLogin: () => dismissDialog(), + case SettingsPage.account: + return SettingsAccountView( + userProfile: user, didLogout: didLogout, - didOpenUser: restartApp, - ); - case SettingsPage.notifications: - return const SettingsNotificationsView(); - case SettingsPage.cloud: - return SettingCloud( - restartAppFlowy: () => restartApp(), + didLogin: dismissDialog, ); - case SettingsPage.shortcuts: - return const SettingsCustomizeShortcutsWrapper(); - case SettingsPage.member: - return WorkspaceMembersPage(userProfile: user); - case SettingsPage.featureFlags: - return const FeatureFlagsPage(); + // case SettingsPage.language: + // return const SettingsLanguageView(); + // case SettingsPage.files: + // return const SettingsFileSystemView(); + // case SettingsPage.user: + // return SettingsUserView( + // user, + // didLogin: dismissDialog, + // didLogout: didLogout, + // didOpenUser: restartApp, + // ); + // case SettingsPage.notifications: + // return const SettingsNotificationsView(); + // case SettingsPage.cloud: + // return SettingCloud( + // restartAppFlowy: () => restartApp(), + // ); + // case SettingsPage.shortcuts: + // return const SettingsCustomizeShortcutsWrapper(); + // case SettingsPage.member: + // return WorkspaceMembersPage(userProfile: user); + // case SettingsPage.featureFlags: + // return const FeatureFlagsPage(); default: return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart new file mode 100644 index 000000000000..48d788186e67 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; + +class SettingsAlertDialog extends StatefulWidget { + const SettingsAlertDialog({ + super.key, + required this.title, + this.subtitle, + this.children, + this.cancel, + this.confirm, + this.confirmLabel, + this.hideCancelButton = false, + this.isDangerous = false, + }); + + final String title; + final String? subtitle; + final List? children; + final void Function()? cancel; + final void Function()? confirm; + final String? confirmLabel; + final bool hideCancelButton; + final bool isDangerous; + + @override + State createState() => _SettingsAlertDialogState(); +} + +class _SettingsAlertDialogState extends State { + @override + Widget build(BuildContext context) { + return StyledDialog( + maxHeight: 600, + maxWidth: 600, + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: FlowyText.medium( + widget.title, + fontSize: 22, + color: Theme.of(context).colorScheme.tertiary, + maxLines: null, + ), + ), + ], + ), + if (widget.subtitle?.isNotEmpty ?? false) ...[ + const VSpace(16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: FlowyText.regular( + widget.subtitle!, + fontSize: 16, + color: Theme.of(context).colorScheme.tertiary, + maxLines: null, + ), + ), + ], + ), + ], + if (widget.children?.isNotEmpty ?? false) ...[ + const VSpace(16), + ...widget.children!, + ], + if (widget.confirm != null || !widget.hideCancelButton) ...[ + const VSpace(20), + ], + _Actions( + hideCancelButton: widget.hideCancelButton, + confirmLabel: widget.confirmLabel, + cancel: widget.cancel, + confirm: widget.confirm, + isDangerous: widget.isDangerous, + ), + ], + ), + ); + } +} + +class _Actions extends StatelessWidget { + const _Actions({ + required this.hideCancelButton, + this.confirmLabel, + this.cancel, + this.confirm, + this.isDangerous = false, + }); + + final bool hideCancelButton; + final String? confirmLabel; + final VoidCallback? cancel; + final VoidCallback? confirm; + final bool isDangerous; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!hideCancelButton) ...[ + SizedBox( + height: 24, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontColor: AFThemeExtension.of(context).textColor, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + onPressed: () { + cancel?.call(); + Navigator.of(context).pop(); + }, + ), + ), + ], + if (confirm != null && !hideCancelButton) ...[ + const HSpace(8), + ], + if (confirm != null) ...[ + SizedBox( + height: 48, + child: FlowyTextButton( + confirmLabel ?? LocaleKeys.button_confirm.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontColor: isDangerous ? Colors.white : null, + fontHoverColor: Colors.white, + fillColor: isDangerous + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + hoverColor: isDangerous + ? Theme.of(context).colorScheme.error + : const Color(0xFF005483), + onPressed: confirm, + ), + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart new file mode 100644 index 000000000000..b8fb8c38dc82 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +/// Renders a simple category taking a title and the list +/// of children (settings) to be rendered. +/// +class SettingsCategory extends StatelessWidget { + const SettingsCategory({ + super.key, + required this.title, + required this.children, + }); + + final String title; + + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + title, + maxLines: 2, + fontSize: 16, + overflow: TextOverflow.ellipsis, + ), + const VSpace(8), + SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => + children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart new file mode 100644 index 000000000000..2ef45eadad55 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +/// This is used to create a uniform space and divider +/// between categories in settings. +/// +class SettingsCategorySpacer extends StatelessWidget { + const SettingsCategorySpacer({super.key}); + + @override + Widget build(BuildContext context) { + return const Divider( + height: 32, + color: Color(0xFFF2F2F2), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart new file mode 100644 index 000000000000..48bc205833ef --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +/// Renders a simple header for the settings view +/// +class SettingsHeader extends StatelessWidget { + const SettingsHeader({ + super.key, + required this.title, + this.description, + }); + + final String title; + final String? description; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + title, + fontSize: 24, + ), + if (description?.isNotEmpty == true) ...[ + const VSpace(8), + FlowyText( + description!, + maxLines: 4, + fontSize: 12, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ], + const VSpace(16), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart new file mode 100644 index 000000000000..3841466920da --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; + +/// This is used to describe a settings input field +/// +/// The input will have secondary action of "save" and "cancel" +/// which will only be shown when the input has changed. +/// +/// _Note: The label can overflow and will be ellipsized._ +/// +class SettingsInputField extends StatefulWidget { + const SettingsInputField({ + super.key, + this.label, + this.focusNode, + this.obscureText = false, + this.value, + this.placeholder, + this.tooltip, + this.onSave, + this.onCancel, + }); + + final String? label; + + final FocusNode? focusNode; + + /// If true, the input field will be obscured + /// and an option to toggle to show the text will be provided. + /// + final bool obscureText; + + final String? value; + final String? placeholder; + final String? tooltip; + + final Function(String)? onSave; + + /// The action to be performed when the cancel button is pressed. + /// + /// If null the button will **NOT** be disabled! Instead it will + /// reset the input to the original value. + /// + final Function()? onCancel; + + @override + State createState() => _SettingsInputFieldState(); +} + +class _SettingsInputFieldState extends State { + late final controller = TextEditingController(text: widget.value); + late bool obscureText = widget.obscureText; + late final FocusNode focusNode = widget.focusNode ?? FocusNode(); + + @override + void dispose() { + if (widget.focusNode == null) { + focusNode.dispose(); + } + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + if (widget.label?.isNotEmpty == true) ...[ + Flexible( + child: FlowyText.medium( + widget.label!, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ), + ], + if (widget.tooltip != null) ...[ + const HSpace(4), + FlowyTooltip( + message: widget.tooltip, + child: const FlowySvg(FlowySvgs.information_s), + ), + ], + ], + ), + const VSpace(8), + SizedBox( + height: 48, + child: FlowyTextField( + focusNode: focusNode, + controller: controller, + autoFocus: false, + obscureText: obscureText, + isDense: false, + suffixIconConstraints: + BoxConstraints.tight(const Size(23 + 18, 24)), + suffixIcon: !widget.obscureText + ? null + : GestureDetector( + onTap: () { + setState(() => obscureText = !obscureText); + }, + child: Padding( + padding: const EdgeInsets.only(right: 18), + child: FlowySvg( + obscureText ? FlowySvgs.show_m : FlowySvgs.hide_m, + size: const Size(12, 15), + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + onSubmitted: widget.onSave, + onChanged: (_) => setState(() {}), + ), + ), + if ((widget.value == null && controller.text.isNotEmpty) || + widget.value != null && widget.value != controller.text) ...[ + const VSpace(8), + Row( + children: [ + const Spacer(), + SizedBox( + height: 21, + child: FlowyTextButton( + LocaleKeys.button_save.tr(), + fontWeight: FontWeight.normal, + padding: EdgeInsets.zero, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + onPressed: () => widget.onSave?.call(controller.text), + ), + ), + const HSpace(24), + SizedBox( + height: 21, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + fontWeight: FontWeight.normal, + padding: EdgeInsets.zero, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + onPressed: () { + setState(() => controller.text = widget.value ?? ''); + widget.onCancel?.call(); + }, + ), + ), + ], + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart new file mode 100644 index 000000000000..105fddd85db6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +/// Renders a simple category taking a title and the list +/// of children (settings) to be rendered. +/// +class SettingsSubcategory extends StatelessWidget { + const SettingsSubcategory({ + super.key, + required this.title, + required this.children, + }); + + final String title; + + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + title, + color: AFThemeExtension.of(context).secondaryTextColor, + maxLines: 2, + fontSize: 14, + overflow: TextOverflow.ellipsis, + ), + const VSpace(8), + SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => + children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart new file mode 100644 index 000000000000..4fcd30e25d76 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +/// This is used to describe a single setting action +/// +/// This will render a simple action that takes the title, +/// the button label, and the button action. +/// +/// _Note: The label can overflow and will be ellipsized, +/// unless maxLines is overriden._ +/// +class SingleSettingAction extends StatelessWidget { + const SingleSettingAction({ + super.key, + required this.label, + this.labelMaxLines, + required this.buttonLabel, + this.onPressed, + this.isDangerous = false, + this.fontSize = 14, + }); + + final String label; + final int? labelMaxLines; + final String buttonLabel; + + /// The action to be performed when the button is pressed + /// + /// If null the button will be rendered as disabled. + /// + final VoidCallback? onPressed; + + /// If isDangerous is true, the button will be rendered as a dangerous + /// action, with a red outline. + /// + final bool isDangerous; + + final double fontSize; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.medium( + label, + fontSize: fontSize, + maxLines: labelMaxLines, + overflow: TextOverflow.ellipsis, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ), + const HSpace(24), + SizedBox( + height: 32, + child: FlowyTextButton( + buttonLabel, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + fillColor: + isDangerous ? null : Theme.of(context).colorScheme.primary, + hoverColor: isDangerous ? null : const Color(0xFF005483), + fontColor: isDangerous ? Theme.of(context).colorScheme.error : null, + fontHoverColor: Colors.white, + fontSize: 12, + isDangerous: isDangerous, + onPressed: onPressed, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index e9ccc32b0e05..e623652f8baa 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; @@ -10,7 +12,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingThirdPartyLogin extends StatelessWidget { @@ -42,24 +43,12 @@ class SettingThirdPartyLogin extends StatelessWidget { : const SizedBox.shrink(); return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.signIn_signInWith.tr(), - fontSize: 16, - ), - const HSpace(6), - ], - ), - const VSpace(6), promptMessage, const VSpace(6), indicator, const VSpace(6), if (isAuthEnabled) const ThirdPartySignInButtons(), - const VSpace(6), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index f9ae9b3124a9..0776766e3b30 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,11 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; class SettingsMenu extends StatelessWidget { const SettingsMenu({ @@ -19,78 +19,107 @@ class SettingsMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: SeparatedColumn( - separatorBuilder: () => const SizedBox(height: 10), - children: [ - SettingsMenuElement( - page: SettingsPage.appearance, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_appearance.tr(), - icon: Icons.brightness_4, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.language, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_language.tr(), - icon: Icons.translate, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.files, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_files.tr(), - icon: Icons.file_present_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.user, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_user.tr(), - icon: Icons.account_box_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.notifications, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_notifications.tr(), - icon: Icons.notifications_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.cloud, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: Icons.sync, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.shortcuts, - selectedPage: currentPage, - label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), - icon: Icons.cut, - changeSelectedPage: changeSelectedPage, - ), - if (FeatureFlag.membersSettings.isOn) - SettingsMenuElement( - page: SettingsPage.member, - selectedPage: currentPage, - label: LocaleKeys.settings_appearance_members_label.tr(), - icon: Icons.people, - changeSelectedPage: changeSelectedPage, + return Column( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 8, right: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), ), - if (kDebugMode) - SettingsMenuElement( - // no need to translate this page - page: SettingsPage.featureFlags, - selectedPage: currentPage, - label: 'Feature Flags', - icon: Icons.flag, - changeSelectedPage: changeSelectedPage, + child: SingleChildScrollView( + // Right padding is added to make the scrollbar centered + // in the space between the menu and the content + padding: const EdgeInsets.only(right: 4) + + const EdgeInsets.symmetric(vertical: 16), + physics: const ClampingScrollPhysics(), + child: SeparatedColumn( + separatorBuilder: () => const SizedBox(height: 16), + children: [ + SettingsMenuElement( + page: SettingsPage.account, + selectedPage: currentPage, + label: LocaleKeys.settings_account_menuLabel.tr(), + icon: FlowySvgs.settings_account_m, + changeSelectedPage: changeSelectedPage, + ), + // SettingsMenuElement( + // page: SettingsPage.appearance, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_appearance.tr(), + // icon: Icons.brightness_4, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.language, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_language.tr(), + // icon: Icons.translate, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.files, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_files.tr(), + // icon: Icons.file_present_outlined, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.user, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_user.tr(), + // icon: Icons.account_box_outlined, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.notifications, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_notifications.tr(), + // icon: Icons.notifications_outlined, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.cloud, + // selectedPage: currentPage, + // label: LocaleKeys.settings_menu_cloudSettings.tr(), + // icon: Icons.sync, + // changeSelectedPage: changeSelectedPage, + // ), + // SettingsMenuElement( + // page: SettingsPage.shortcuts, + // selectedPage: currentPage, + // label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), + // icon: Icons.cut, + // changeSelectedPage: changeSelectedPage, + // ), + // if (FeatureFlag.membersSettings.isOn) + // SettingsMenuElement( + // page: SettingsPage.member, + // selectedPage: currentPage, + // label: LocaleKeys.settings_appearance_members_label.tr(), + // icon: Icons.people, + // changeSelectedPage: changeSelectedPage, + // ), + // if (kDebugMode) + // SettingsMenuElement( + // // no need to translate this page + // page: SettingsPage.featureFlags, + // selectedPage: currentPage, + // label: 'Feature Flags', + // icon: Icons.flag, + // changeSelectedPage: changeSelectedPage, + // ), + ], + ), ), - ], - ), + ), + ), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart index ad02cd9df62b..18b8d82c91dc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart @@ -1,8 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; class SettingsMenuElement extends StatelessWidget { const SettingsMenuElement({ @@ -17,27 +20,22 @@ class SettingsMenuElement extends StatelessWidget { final SettingsPage page; final SettingsPage selectedPage; final String label; - final IconData icon; + final FlowySvgData icon; final Function changeSelectedPage; @override Widget build(BuildContext context) { return FlowyHover( + isSelected: () => page == selectedPage, resetHoverOnRebuild: false, style: HoverStyle( - hoverColor: Theme.of(context).colorScheme.primary, + hoverColor: AFThemeExtension.of(context).greySelect, + borderRadius: BorderRadius.circular(4), ), child: ListTile( - leading: Icon( - icon, - size: 16, - color: page == selectedPage - ? Theme.of(context).colorScheme.onSurface - : null, - ), - onTap: () { - changeSelectedPage(page); - }, + dense: true, + leading: FlowySvg(icon), + onTap: () => changeSelectedPage(page), selected: page == selectedPage, selectedColor: Theme.of(context).colorScheme.onSurface, selectedTileColor: Theme.of(context).colorScheme.primary, @@ -45,7 +43,7 @@ class SettingsMenuElement extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), minLeadingWidth: 0, - title: FlowyText.semibold( + title: FlowyText.medium( label, fontSize: FontSizes.s14, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index c8634b3df5ef..633963cb0a78 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -108,7 +108,7 @@ class _MoreViewActionsState extends State { FlowySvgs.details_s, size: const Size(18, 18), color: isHovering - ? Theme.of(context).colorScheme.onPrimary + ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).iconTheme.color, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 66ccbe01e0eb..4f2eaba0f81e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,3 +1,5 @@ +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'; @@ -6,10 +8,9 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; const double _smallSize = 28; -const double _largeSize = 56; +const double _largeSize = 64; class UserAvatar extends StatelessWidget { const UserAvatar({ @@ -17,12 +18,16 @@ class UserAvatar extends StatelessWidget { required this.iconUrl, required this.name, this.isLarge = false, + this.isHovering = false, }); final String iconUrl; final String name; final bool isLarge; + // If true, a border will be applied on top of the avatar + final bool isHovering; + @override Widget build(BuildContext context) { final size = isLarge ? _largeSize : _smallSize; @@ -47,6 +52,12 @@ class UserAvatar extends StatelessWidget { decoration: BoxDecoration( color: color, shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, ), child: FlowyText.semibold( nameInitials, @@ -64,16 +75,27 @@ class UserAvatar extends StatelessWidget { return SizedBox.square( dimension: size, - child: ClipRRect( - borderRadius: Corners.s5Border, - child: CircleAvatar( - backgroundColor: Colors.transparent, - child: builtInSVGIcons.contains(iconUrl) - ? FlowySvg( - FlowySvgData('emoji/$iconUrl'), - blendMode: null, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, ) - : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: builtInSVGIcons.contains(iconUrl) + ? FlowySvg( + FlowySvgData('emoji/$iconUrl'), + blendMode: null, + ) + : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + ), ), ), ); @@ -81,6 +103,15 @@ class UserAvatar extends StatelessWidget { /// Return the user name, if the user name is empty, /// return the default user name. + /// String _userName(String name) => name.isEmpty ? LocaleKeys.defaultUsername.tr() : name; + + /// Used to darken the generated color for the hover border effect. + /// The color is darkened by 15% - Hence the 0.15 value. + /// + Color _darken(Color color) { + final hsl = HSLColor.fromColor(color); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 582109a5d3f5..caba18213194 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -1,10 +1,11 @@ -import 'package:flowy_infra/utils/color_converter.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra/utils/color_converter.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'default_colorscheme.dart'; + import 'dandelion.dart'; +import 'default_colorscheme.dart'; import 'lavender.dart'; import 'lemonade.dart'; @@ -74,6 +75,7 @@ class FlowyColorScheme { final Color topbarBg; final Color icon; final Color text; + final Color secondaryText; final Color input; final Color hint; final Color primary; @@ -93,6 +95,7 @@ class FlowyColorScheme { final Color calendarWeekendBGColor; //grid bottom count color final Color gridRowCountColor; + const FlowyColorScheme({ required this.surface, required this.hover, @@ -128,6 +131,7 @@ class FlowyColorScheme { required this.topbarBg, required this.icon, required this.text, + required this.secondaryText, required this.input, required this.hint, required this.primary, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index 684e876704fd..7c3939e5feae 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -63,6 +63,7 @@ class DandelionColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -117,6 +118,7 @@ class DandelionColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 706d7cc3ad4d..b28883209653 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -58,6 +58,7 @@ class DefaultColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: const Color(0xff4f4f4f), input: _white, hint: _lightShader3, primary: _lightMain1, @@ -89,7 +90,7 @@ class DefaultColorScheme extends FlowyColorScheme { shader5: _darkShader5, shader6: _darkShader6, shader7: _white, - bg1: const Color(0xffF7F8FC), + bg1: const Color(0xff1A202C), bg2: const Color(0xffEDEEF2), bg3: _darkMain1, bg4: const Color(0xff2C144B), @@ -110,6 +111,7 @@ class DefaultColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: const Color(0xff59647a), primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 894fc4c4b2ed..87fea48cdd86 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -61,6 +61,7 @@ class LavenderColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightMain1, @@ -113,6 +114,7 @@ class LavenderColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 86a548059e1b..d2a80f7b92ec 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -65,6 +65,7 @@ class LemonadeColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -119,6 +120,7 @@ class LemonadeColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 69ae7582f94f..38e7793dde27 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -16,6 +16,7 @@ class AFThemeExtension extends ThemeExtension { final Color tint9; final Color textColor; + final Color secondaryTextColor; final Color greyHover; final Color greySelect; final Color lightGreyHover; @@ -48,6 +49,7 @@ class AFThemeExtension extends ThemeExtension { required this.lightGreyHover, required this.toggleOffFill, required this.textColor, + required this.secondaryTextColor, required this.calloutBGColor, required this.tableCellBGColor, required this.calendarWeekendBGColor, @@ -77,6 +79,7 @@ class AFThemeExtension extends ThemeExtension { Color? tint8, Color? tint9, Color? textColor, + Color? secondaryTextColor, Color? calloutBGColor, Color? tableCellBGColor, Color? greyHover, @@ -104,6 +107,7 @@ class AFThemeExtension extends ThemeExtension { tint8: tint8 ?? this.tint8, tint9: tint9 ?? this.tint9, textColor: textColor ?? this.textColor, + secondaryTextColor: secondaryTextColor ?? this.secondaryTextColor, calloutBGColor: calloutBGColor ?? this.calloutBGColor, tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor, greyHover: greyHover ?? this.greyHover, @@ -140,6 +144,11 @@ class AFThemeExtension extends ThemeExtension { tint8: Color.lerp(tint8, other.tint8, t)!, tint9: Color.lerp(tint9, other.tint9, t)!, textColor: Color.lerp(textColor, other.textColor, t)!, + secondaryTextColor: Color.lerp( + secondaryTextColor, + other.secondaryTextColor, + t, + )!, calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!, tableCellBGColor: Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index dced70a6bea0..532fdc8668d0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -149,6 +148,7 @@ class FlowyTextButton extends StatelessWidget { final String text; final FontWeight? fontWeight; final Color? fontColor; + final Color? fontHoverColor; final double? fontSize; final TextOverflow overflow; @@ -165,6 +165,7 @@ class FlowyTextButton extends StatelessWidget { final TextDecoration? decoration; final String? fontFamily; + final bool isDangerous; // final HoverDisplayConfig? hoverDisplay; const FlowyTextButton( @@ -173,6 +174,7 @@ class FlowyTextButton extends StatelessWidget { this.onPressed, this.fontSize, this.fontColor, + this.fontHoverColor, this.overflow = TextOverflow.ellipsis, this.fontWeight, this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -185,6 +187,7 @@ class FlowyTextButton extends StatelessWidget { this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0), this.decoration, this.fontFamily, + this.isDangerous = false, }); @override @@ -195,15 +198,10 @@ class FlowyTextButton extends StatelessWidget { children.add(const HSpace(8)); } children.add( - FlowyText( + Text( text, overflow: overflow, - fontWeight: fontWeight, - fontSize: fontSize, - color: fontColor, textAlign: TextAlign.center, - decoration: decoration, - fontFamily: fontFamily, ), ); @@ -213,23 +211,64 @@ class FlowyTextButton extends StatelessWidget { children: children, ); - child = RawMaterialButton( - focusNode: FocusNode(skipTraversal: onPressed == null), - hoverElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), - fillColor: fillColor ?? Theme.of(context).colorScheme.secondaryContainer, - hoverColor: - hoverColor ?? Theme.of(context).colorScheme.secondaryContainer, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, + child = ConstrainedBox( constraints: constraints, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: padding, - onPressed: onPressed, - child: child, + child: TextButton( + onPressed: onPressed ?? () {}, + focusNode: FocusNode(skipTraversal: onPressed == null), + style: ButtonStyle( + overlayColor: const MaterialStatePropertyAll(Colors.transparent), + minimumSize: MaterialStateProperty.all(Size.zero), + splashFactory: NoSplash.splashFactory, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: MaterialStateProperty.all(padding), + elevation: MaterialStateProperty.all(0), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + side: BorderSide( + color: isDangerous + ? Theme.of(context).colorScheme.error + : Colors.transparent, + ), + borderRadius: radius ?? Corners.s6Border, + ), + ), + textStyle: MaterialStateProperty.all( + TextStyle( + fontWeight: fontWeight ?? FontWeight.w500, + fontSize: fontSize, + decoration: decoration, + fontFamily: fontFamily, + ), + ), + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.hovered)) { + return hoverColor ?? + (isDangerous + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary); + } + + return fillColor ?? + (isDangerous + ? Colors.transparent + : Theme.of(context).colorScheme.secondaryContainer); + }, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.hovered)) { + return fontHoverColor ?? + (fontColor ?? Theme.of(context).colorScheme.onSurface); + } + + return fontColor ?? Theme.of(context).colorScheme.onSurface; + }, + ), + ), + child: child, + ), ); if (tooltip != null) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index dd6e4d56e15e..a3da4dad94d7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_infra/size.dart'; + class FlowyTextField extends StatefulWidget { final String? hintText; final String? text; @@ -32,6 +33,8 @@ class FlowyTextField extends StatefulWidget { final TextAlignVertical? textAlignVertical; final TextInputType? keyboardType; final List? inputFormatters; + final bool obscureText; + final bool isDense; const FlowyTextField({ super.key, @@ -62,6 +65,8 @@ class FlowyTextField extends StatefulWidget { this.textAlignVertical, this.keyboardType = TextInputType.multiline, this.inputFormatters, + this.obscureText = false, + this.isDense = true, }); @override @@ -155,6 +160,7 @@ class FlowyTextFieldState extends State { textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center, keyboardType: widget.keyboardType, inputFormatters: widget.inputFormatters, + obscureText: widget.obscureText, decoration: widget.decoration ?? InputDecoration( constraints: widget.hintTextConstraints ?? @@ -162,14 +168,13 @@ class FlowyTextFieldState extends State { maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58, ), contentPadding: EdgeInsets.symmetric( - horizontal: 12, + horizontal: widget.isDense ? 12 : 18, vertical: (widget.maxLines == null || widget.maxLines! > 1) ? 12 : 0, ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.outline, - width: 1.0, ), borderRadius: Corners.s8Border, ), @@ -190,21 +195,18 @@ class FlowyTextFieldState extends State { focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, - width: 1.0, ), borderRadius: Corners.s8Border, ), errorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, - width: 1.0, ), borderRadius: Corners.s8Border, ), focusedErrorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, - width: 1.0, ), borderRadius: Corners.s8Border, ), diff --git a/frontend/resources/flowy_icons/24x/settings_account.svg b/frontend/resources/flowy_icons/24x/settings_account.svg new file mode 100644 index 000000000000..c64b21ec04f0 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_account.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/settings_data.svg b/frontend/resources/flowy_icons/24x/settings_data.svg new file mode 100644 index 000000000000..0d0934cb5279 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_data.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_members.svg b/frontend/resources/flowy_icons/24x/settings_members.svg new file mode 100644 index 000000000000..ad970d47090e --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_members.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_notifications.svg b/frontend/resources/flowy_icons/24x/settings_notifications.svg new file mode 100644 index 000000000000..5fc6d58d556f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_notifications.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_plan.svg b/frontend/resources/flowy_icons/24x/settings_plan.svg new file mode 100644 index 000000000000..5c6f53f8368d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_plan.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_shortcuts.svg b/frontend/resources/flowy_icons/24x/settings_shortcuts.svg new file mode 100644 index 000000000000..aa2765c1f786 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_shortcuts.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_sync.svg b/frontend/resources/flowy_icons/24x/settings_sync.svg new file mode 100644 index 000000000000..bdd0b49356c0 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_sync.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_workplace.svg b/frontend/resources/flowy_icons/24x/settings_workplace.svg new file mode 100644 index 000000000000..4c9b4d9bad63 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_workplace.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 164c64aa0652..f3d37ba28601 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -223,7 +223,8 @@ "editContact": "Edit Contact" }, "button": { - "ok": "OK", + "ok": "Ok", + "confirm": "Confirm", "done": "Done", "cancel": "Cancel", "signIn": "Sign In", @@ -286,6 +287,31 @@ }, "settings": { "title": "Settings", + "account": { + "menuLabel": "My account", + "title": "My account", + "description": "Customize your profile, manage account security, open AI keys, or login into your account.", + "general": { + "title": "Account name & profile image", + "changeProfilePicture": "Change" + }, + "email": { + "title": "Email", + "actions": { + "change": "Change email" + } + }, + "keys": { + "title": "AI Keys", + "openAILabel": "OpenAI key", + "openAITooltip": "The OpenAPI API key to use for the AI models" + }, + "login": { + "title": "Account login", + "loginLabel": "Log in", + "logoutLabel": "Log out" + } + }, "menu": { "appearance": "Appearance", "language": "Language", @@ -294,7 +320,7 @@ "notifications": "Notifications", "open": "Open Settings", "logout": "Logout", - "logoutPrompt": "Are you sure to logout?", + "logoutPrompt": "Are you sure you want to log out?", "selfEncryptionLogoutPrompt": "Are you sure you want to log out? Please ensure you have copied the encryption secret", "syncSetting": "Sync Setting", "cloudSettings": "Cloud Settings", @@ -1406,4 +1432,4 @@ } } } -} \ No newline at end of file +} From 4fd0b31ce4022f108990025444cf48f07690dd10 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 19 Mar 2024 12:37:27 +0100 Subject: [PATCH 02/16] chore: remove unused files --- .../presentation/base/box_container.dart | 27 --- .../plugins/base/emoji/emoji_picker_i18n.dart | 41 ---- .../toolbar/board_setting_bloc.dart | 43 ---- .../board/tests/integrate_test/card_test.dart | 16 -- .../database/domain/database_service.dart | 16 -- .../row/accessory/cell_decoration.dart | 10 - .../actions/option_action_button.dart | 147 ------------ .../editor_plugins/infra/svg.dart | 55 ----- .../mobile_align_toolbar_item.dart | 108 --------- .../mobile_block_settings_toolbar_item.dart | 134 ----------- .../mobile_convert_block_toolbar_item.dart | 220 ------------------ .../mobile_indent_toolbar_item.dart | 24 -- .../mobile_text_decoration_item.dart | 217 ----------------- .../mobile_text_decoration_item_v2.dart | 217 ----------------- .../undo_redo/redo_mobile_toolbar_item.dart | 11 - .../undo_redo/undo_mobile_toolbar_item.dart | 11 - .../presentation/export_page_widget.dart | 38 --- .../lib/plugins/trash/menu.dart | 60 ----- .../lib/shared/cloud_image_checker.dart | 20 -- .../lib/util/base64_string.dart | 5 - .../appflowy_flutter/lib/util/json_print.dart | 8 - .../workspace/application/home/prelude.dart | 1 - .../settings/widgets/utils/form_factor.dart | 19 -- 23 files changed, 1448 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart delete mode 100755 frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/trash/menu.dart delete mode 100644 frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart delete mode 100644 frontend/appflowy_flutter/lib/util/base64_string.dart delete mode 100644 frontend/appflowy_flutter/lib/util/json_print.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/home/prelude.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart deleted file mode 100644 index aed96d6944e3..000000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class FlowyBoxContainer extends StatelessWidget { - const FlowyBoxContainer({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 8.0, - ), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.onSecondary, - ), - borderRadius: BorderRadius.circular(8.0), - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart deleted file mode 100644 index 13ba942f4947..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; - -class FlowyEmojiPickerI18n extends EmojiPickerI18n { - @override - String get activity => LocaleKeys.emoji_categories_activities.tr(); - - @override - String get flags => LocaleKeys.emoji_categories_flags.tr(); - - @override - String get foods => LocaleKeys.emoji_categories_food.tr(); - - @override - String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr(); - - @override - String get nature => LocaleKeys.emoji_categories_nature.tr(); - - @override - String get objects => LocaleKeys.emoji_categories_objects.tr(); - - @override - String get people => LocaleKeys.emoji_categories_smileys.tr(); - - @override - String get places => LocaleKeys.emoji_categories_places.tr(); - - @override - String get search => LocaleKeys.emoji_search.tr(); - - @override - String get symbols => LocaleKeys.emoji_categories_symbols.tr(); - - @override - String get searchHintText => LocaleKeys.emoji_search.tr(); - - @override - String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr(); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart deleted file mode 100644 index ea2e1b93147e..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'board_setting_bloc.freezed.dart'; - -class BoardSettingBloc extends Bloc { - BoardSettingBloc({required this.viewId}) - : super(BoardSettingState.initial()) { - on( - (event, emit) async { - event.when( - performAction: (action) { - emit(state.copyWith(selectedAction: action)); - }, - ); - }, - ); - } - - final String viewId; -} - -@freezed -class BoardSettingEvent with _$BoardSettingEvent { - const factory BoardSettingEvent.performAction(BoardSettingAction action) = - _PerformAction; -} - -@freezed -class BoardSettingState with _$BoardSettingState { - const factory BoardSettingState({ - required BoardSettingAction? selectedAction, - }) = _BoardSettingState; - - factory BoardSettingState.initial() => const BoardSettingState( - selectedAction: null, - ); -} - -enum BoardSettingAction { - properties, - groups, -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart b/frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart deleted file mode 100644 index 29308d84dc91..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// import 'package:flutter_test/flutter_test.dart'; -// import 'package:integration_test/integration_test.dart'; -// import 'package:appflowy/main.dart' as app; - -// void main() { -// IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - -// group('end-to-end test', () { -// testWidgets('tap on the floating action button, verify counter', -// (tester) async { -// app.main(); - -// await tester.pumpAndSettle(); -// }); -// }); -// } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart deleted file mode 100644 index 8d237f911445..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class DatabaseBackendService { - static Future, FlowyError>> - getAllDatabases() { - return DatabaseEventGetDatabases().send().then((result) { - return result.fold( - (l) => FlowyResult.success(l.items), - (r) => FlowyResult.failure(r), - ); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart deleted file mode 100755 index 0e74073e143f..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class CellDecoration { - static BoxDecoration box({required Color color}) { - return BoxDecoration( - border: Border.all(color: Colors.black26, width: 0.2), - color: color, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart deleted file mode 100644 index 15e2610bdcd9..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; -import 'package:flutter/material.dart'; - -class OptionActionList extends StatelessWidget { - const OptionActionList({ - super.key, - required this.blockComponentContext, - required this.blockComponentState, - required this.actions, - required this.editorState, - }); - - final BlockComponentContext blockComponentContext; - final BlockComponentActionState blockComponentState; - final List actions; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final popoverActions = actions.map((e) { - if (e == OptionAction.divider) { - return DividerOptionAction(); - } else if (e == OptionAction.color) { - return ColorOptionAction( - editorState: editorState, - ); - } else if (e == OptionAction.depth) { - return DepthOptionAction( - editorState: editorState, - ); - } else { - return OptionActionWrapper(e); - } - }).toList(); - - return PopoverActionList( - popoverMutex: PopoverMutex(), - direction: PopoverDirection.leftWithCenterAligned, - actions: popoverActions, - onPopupBuilder: () => blockComponentState.alwaysShowActions = true, - onClosed: () { - editorState.selectionType = null; - editorState.selection = null; - blockComponentState.alwaysShowActions = false; - }, - onSelected: (action, controller) { - if (action is OptionActionWrapper) { - _onSelectAction(action.inner); - controller.close(); - } - }, - buildChild: (controller) => OptionActionButton( - onTap: () { - controller.show(); - - // update selection - _updateBlockSelection(); - }, - ), - ); - } - - void _updateBlockSelection() { - final startNode = blockComponentContext.node; - var endNode = startNode; - while (endNode.children.isNotEmpty) { - endNode = endNode.children.last; - } - - final start = Position(path: startNode.path); - final end = endNode.selectable?.end() ?? - Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ); - - editorState.selectionType = SelectionType.block; - editorState.selection = Selection( - start: start, - end: end, - ); - } - - void _onSelectAction(OptionAction action) { - final node = blockComponentContext.node; - final transaction = editorState.transaction; - switch (action) { - case OptionAction.delete: - transaction.deleteNode(node); - break; - case OptionAction.duplicate: - transaction.insertNode( - node.path.next, - node.copyWith(), - ); - break; - case OptionAction.turnInto: - break; - case OptionAction.moveUp: - transaction.moveNode(node.path.previous, node); - break; - case OptionAction.moveDown: - transaction.moveNode(node.path.next.next, node); - break; - case OptionAction.align: - case OptionAction.color: - case OptionAction.divider: - case OptionAction.depth: - throw UnimplementedError(); - } - editorState.apply(transaction); - } -} - -class OptionActionButton extends StatelessWidget { - const OptionActionButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Align( - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: IgnoreParentGestureWidget( - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.deferToChild, - child: FlowySvg( - FlowySvgs.drag_element_s, - size: const Size.square(24.0), - color: Theme.of(context).iconTheme.color, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart deleted file mode 100644 index 7c5f0b454852..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class Svg extends StatelessWidget { - const Svg({ - super.key, - this.name, - this.width, - this.height, - this.color, - this.number, - this.padding, - }); - - final String? name; - final double? width; - final double? height; - final Color? color; - final int? number; - final EdgeInsets? padding; - - final _defaultWidth = 20.0; - final _defaultHeight = 20.0; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding ?? const EdgeInsets.all(0), - child: _buildSvg(), - ); - } - - Widget _buildSvg() { - if (name != null) { - return SvgPicture.asset( - 'assets/images/$name.svg', - colorFilter: - color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null, - fit: BoxFit.fill, - height: height, - width: width, - package: 'appflowy_editor_plugins', - ); - } else if (number != null) { - final numberText = - '$number.'; - return SvgPicture.string( - numberText, - width: width ?? _defaultWidth, - height: height ?? _defaultHeight, - ); - } - return const SizedBox.shrink(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart deleted file mode 100644 index 38e04071858b..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -final mobileAlignToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const FlowySvg( - FlowySvgs.toolbar_align_center_s, - size: Size.square(32), - ) - : null; - }, - itemMenuBuilder: (_, editorState, ___) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - return _MobileAlignMenu( - editorState: editorState, - selection: selection, - ); - }, -); - -class _MobileAlignMenu extends StatelessWidget { - const _MobileAlignMenu({ - required this.editorState, - required this.selection, - }); - - final Selection selection; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - return GridView.count( - padding: EdgeInsets.zero, - crossAxisCount: 3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 3, - shrinkWrap: true, - children: [ - _buildAlignmentButton( - context, - 'left', - LocaleKeys.document_plugins_optionAction_left.tr(), - ), - _buildAlignmentButton( - context, - 'center', - LocaleKeys.document_plugins_optionAction_center.tr(), - ), - _buildAlignmentButton( - context, - 'right', - LocaleKeys.document_plugins_optionAction_right.tr(), - ), - ], - ); - } - - Widget _buildAlignmentButton( - BuildContext context, - String alignment, - String label, - ) { - final nodes = editorState.getNodesInSelection(selection); - if (nodes.isEmpty) { - const SizedBox.shrink(); - } - - bool isSatisfyCondition(bool Function(Object? value) test) { - return nodes.every( - (n) => test(n.attributes[blockComponentAlign]), - ); - } - - final data = switch (alignment) { - 'left' => FlowySvgs.toolbar_align_left_s, - 'center' => FlowySvgs.toolbar_align_center_s, - 'right' => FlowySvgs.toolbar_align_right_s, - _ => throw UnimplementedError(), - }; - final isSelected = isSatisfyCondition((value) => value == alignment); - - return MobileToolbarItemMenuBtn( - icon: FlowySvg(data, size: const Size.square(28)), - label: FlowyText(label), - isSelected: isSelected, - onPressed: () async { - await editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: alignment, - }, - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart deleted file mode 100644 index 7ab072533f28..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart'; -import 'package:appflowy/plugins/base/color/color_picker_screen.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final mobileBlockSettingsToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInSingleSelectionAndTextType(editorState) - ? const FlowySvg(FlowySvgs.three_dots_s) - : null; - }, - actionHandler: (_, editorState) async { - // show the settings page - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - final context = node?.context; - if (node == null || context == null) { - return; - } - - await _showBlockActionSheet( - context, - editorState, - node, - selection, - ); - }, -); - -Future _showBlockActionSheet( - BuildContext context, - EditorState editorState, - Node node, - Selection selection, -) async { - final result = await showMobileBottomSheet( - context, - showDragHandle: true, - showCloseButton: true, - showHeader: true, - padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), - title: LocaleKeys.document_plugins_action.tr(), - builder: (context) { - return BlockActionBottomSheet( - extendActionWidgets: [ - Row( - children: [ - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_color_m, - text: LocaleKeys.document_plugins_optionAction_color.tr(), - onTap: () async { - final option = await context.push( - Uri( - path: MobileColorPickerScreen.routeName, - queryParameters: { - MobileColorPickerScreen.pageTitle: LocaleKeys - .document_plugins_optionAction_color - .tr(), - }, - ).toString(), - ); - if (option != null) { - final transaction = editorState.transaction; - transaction.updateNode(node, { - blockComponentBackgroundColor: option.id, - }); - await editorState.apply(transaction); - } - if (context.mounted) { - context.pop(true); - } - }, - ), - ), - // more options ... - ], - ), - ], - onAction: (action) async { - context.pop(true); - - final transaction = editorState.transaction; - switch (action) { - case BlockActionBottomSheetType.delete: - transaction.deleteNode(node); - break; - case BlockActionBottomSheetType.duplicate: - transaction.insertNode( - node.path.next, - node.copyWith(), - ); - break; - case BlockActionBottomSheetType.insertAbove: - case BlockActionBottomSheetType.insertBelow: - final path = action == BlockActionBottomSheetType.insertAbove - ? node.path - : node.path.next; - transaction - ..insertNode( - path, - paragraphNode(), - ) - ..afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - break; - default: - } - - if (transaction.operations.isNotEmpty) { - await editorState.apply(transaction); - } - }, - ); - }, - ); - - if (result != true) { - // restore the selection - editorState.selection = selection; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart deleted file mode 100644 index e3e8c80942d8..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -// convert the current block to other block types -// only show in single selection and text type -final mobileConvertBlockToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState, ___) { - if (!onlyShowInSingleSelectionAndTextType(editorState)) { - return null; - } - return const FlowySvg( - FlowySvgs.convert_s, - size: Size.square(22), - ); - }, - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - return BlocksMenu( - items: _convertToBlockMenuItems, - editorState: editorState, - service: service, - ); - }, -); - -final _convertToBlockMenuItems = [ - // paragraph - BlockMenuItem( - blockType: ParagraphBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_text_decoration_m), - label: LocaleKeys.editor_text.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - ParagraphBlockKeys.type, - selection: selection, - ), - ), - - // to-do list - BlockMenuItem( - blockType: TodoListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_checkbox_m), - label: LocaleKeys.editor_checkbox.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - TodoListBlockKeys.type, - selection: selection, - extraAttributes: { - TodoListBlockKeys.checked: false, - }, - ), - ), - - // heading 1 - 3 - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h1_m), - label: LocaleKeys.editor_heading1.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 1, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 1, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 1, - }, - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h2_m), - label: LocaleKeys.editor_heading2.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 2, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 2, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 2, - }, - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h3_m), - label: LocaleKeys.editor_heading3.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 3, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 3, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 3, - }, - ); - }, - ), - - // bulleted list - BlockMenuItem( - blockType: BulletedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_bulleted_list_m), - label: LocaleKeys.editor_bulletedList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - BulletedListBlockKeys.type, - selection: selection, - ), - ), - - // numbered list - BlockMenuItem( - blockType: NumberedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_numbered_list_m), - label: LocaleKeys.editor_numberedList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - NumberedListBlockKeys.type, - selection: selection, - ), - ), - - // toggle list - BlockMenuItem( - blockType: ToggleListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_toggle_list_m), - label: LocaleKeys.document_plugins_toggleList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - selection: selection, - ToggleListBlockKeys.type, - ), - ), - - // quote - BlockMenuItem( - blockType: QuoteBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_quote_m), - label: LocaleKeys.editor_quote.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - selection: selection, - QuoteBlockKeys.type, - ), - ), - - // callout - BlockMenuItem( - blockType: CalloutBlockKeys.type, - // FIXME: update icon - icon: const Icon(Icons.note_rounded), - label: LocaleKeys.document_plugins_callout.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - CalloutBlockKeys.type, - selection: selection, - extraAttributes: { - CalloutBlockKeys.icon: '📌', - }, - ), - ), - - // code - BlockMenuItem( - blockType: CodeBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_code_m), - label: LocaleKeys.document_selectionMenu_codeBlock.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - CodeBlockKeys.type, - selection: selection, - ), - ), -]; - -bool _isHeadingSelected( - EditorState editorState, - Selection selection, - int level, -) { - final node = editorState.getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - return false; - } - return type == HeadingBlockKeys.type && - node.attributes[HeadingBlockKeys.level] == level; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart deleted file mode 100644 index adc1026f915a..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -final mobileIndentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const Icon(Icons.format_indent_increase_rounded) - : null; - }, - actionHandler: (_, editorState) { - indentCommand.execute(editorState); - }, -); - -final mobileOutdentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const Icon(Icons.format_indent_decrease_rounded) - : null; - }, - actionHandler: (_, editorState) { - outdentCommand.execute(editorState); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart deleted file mode 100644 index dddda0758d16..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.text_s, - size: Size.square(24), - ), - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - return _TextDecorationMenu( - editorState, - selection, - service, - ); - }, -); - -class _TextDecorationMenu extends StatefulWidget { - const _TextDecorationMenu( - this.editorState, - this.selection, - this.service, - ); - - final EditorState editorState; - final Selection selection; - final MobileToolbarWidgetService service; - - @override - State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); -} - -class _TextDecorationMenuState extends State<_TextDecorationMenu> { - EditorState get editorState => widget.editorState; - - final textDecorations = [ - // BIUS - TextDecorationUnit( - icon: AFMobileIcons.bold, - label: AppFlowyEditorL10n.current.bold, - name: AppFlowyRichTextKeys.bold, - ), - TextDecorationUnit( - icon: AFMobileIcons.italic, - label: AppFlowyEditorL10n.current.italic, - name: AppFlowyRichTextKeys.italic, - ), - TextDecorationUnit( - icon: AFMobileIcons.underline, - label: AppFlowyEditorL10n.current.underline, - name: AppFlowyRichTextKeys.underline, - ), - TextDecorationUnit( - icon: AFMobileIcons.strikethrough, - label: AppFlowyEditorL10n.current.strikethrough, - name: AppFlowyRichTextKeys.strikethrough, - ), - - // Code - TextDecorationUnit( - icon: AFMobileIcons.code, - label: AppFlowyEditorL10n.current.embedCode, - name: AppFlowyRichTextKeys.code, - ), - - // link - TextDecorationUnit( - icon: AFMobileIcons.link, - label: AppFlowyEditorL10n.current.link, - name: AppFlowyRichTextKeys.href, - ), - ]; - - @override - void dispose() { - widget.editorState.selectionExtraInfo = null; - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final children = textDecorations - .map((currentDecoration) { - // Check current decoration is active or not - final selection = widget.selection; - - // only show edit link bottom sheet when selection is not collapsed - if (selection.isCollapsed && - currentDecoration.name == AppFlowyRichTextKeys.href) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final bool isSelected; - if (selection.isCollapsed) { - isSelected = editorState.toggledStyle.containsKey( - currentDecoration.name, - ); - } else { - isSelected = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[currentDecoration.name] == true, - ); - }); - } - - return MobileToolbarItemMenuBtn( - icon: AFMobileIcon( - afMobileIcons: currentDecoration.icon, - color: MobileToolbarTheme.of(context).iconColor, - ), - label: FlowyText(currentDecoration.label), - isSelected: isSelected, - onPressed: () { - if (currentDecoration.name == AppFlowyRichTextKeys.href) { - if (selection.isCollapsed) { - return; - } - - _closeKeyboard(); - - // show edit link bottom sheet - final context = nodes.firstOrNull?.context; - if (context != null) { - final text = editorState - .getTextInSelection( - widget.selection, - ) - .join(); - final href = - editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.href, - widget.selection, - ); - showEditLinkBottomSheet( - context, - text, - href, - (context, newText, newHref) { - _updateTextAndHref(text, href, newText, newHref); - context.pop(); - }, - ); - } - } else { - setState(() { - editorState.toggleAttribute(currentDecoration.name); - }); - } - }, - ); - }) - .nonNulls - .toList(); - - return GridView.count( - shrinkWrap: true, - padding: EdgeInsets.zero, - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 4, - children: children, - ); - } - - void _closeKeyboard() { - editorState.updateSelectionWithReason( - widget.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ); - editorState.service.keyboardService?.closeKeyboard(); - } - - void _updateTextAndHref( - String prevText, - String? prevHref, - String text, - String href, - ) async { - final selection = widget.selection; - if (!selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - final transaction = editorState.transaction; - if (prevText != text) { - transaction.replaceText( - node, - selection.startIndex, - selection.length, - text, - ); - } - // if the text is empty, it means the user wants to remove the text - if (text.isNotEmpty && prevHref != href) { - transaction.formatText(node, selection.startIndex, text.length, { - AppFlowyRichTextKeys.href: href.isEmpty ? null : href, - }); - } - await editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart deleted file mode 100644 index dddda0758d16..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.text_s, - size: Size.square(24), - ), - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - return _TextDecorationMenu( - editorState, - selection, - service, - ); - }, -); - -class _TextDecorationMenu extends StatefulWidget { - const _TextDecorationMenu( - this.editorState, - this.selection, - this.service, - ); - - final EditorState editorState; - final Selection selection; - final MobileToolbarWidgetService service; - - @override - State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); -} - -class _TextDecorationMenuState extends State<_TextDecorationMenu> { - EditorState get editorState => widget.editorState; - - final textDecorations = [ - // BIUS - TextDecorationUnit( - icon: AFMobileIcons.bold, - label: AppFlowyEditorL10n.current.bold, - name: AppFlowyRichTextKeys.bold, - ), - TextDecorationUnit( - icon: AFMobileIcons.italic, - label: AppFlowyEditorL10n.current.italic, - name: AppFlowyRichTextKeys.italic, - ), - TextDecorationUnit( - icon: AFMobileIcons.underline, - label: AppFlowyEditorL10n.current.underline, - name: AppFlowyRichTextKeys.underline, - ), - TextDecorationUnit( - icon: AFMobileIcons.strikethrough, - label: AppFlowyEditorL10n.current.strikethrough, - name: AppFlowyRichTextKeys.strikethrough, - ), - - // Code - TextDecorationUnit( - icon: AFMobileIcons.code, - label: AppFlowyEditorL10n.current.embedCode, - name: AppFlowyRichTextKeys.code, - ), - - // link - TextDecorationUnit( - icon: AFMobileIcons.link, - label: AppFlowyEditorL10n.current.link, - name: AppFlowyRichTextKeys.href, - ), - ]; - - @override - void dispose() { - widget.editorState.selectionExtraInfo = null; - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final children = textDecorations - .map((currentDecoration) { - // Check current decoration is active or not - final selection = widget.selection; - - // only show edit link bottom sheet when selection is not collapsed - if (selection.isCollapsed && - currentDecoration.name == AppFlowyRichTextKeys.href) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final bool isSelected; - if (selection.isCollapsed) { - isSelected = editorState.toggledStyle.containsKey( - currentDecoration.name, - ); - } else { - isSelected = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[currentDecoration.name] == true, - ); - }); - } - - return MobileToolbarItemMenuBtn( - icon: AFMobileIcon( - afMobileIcons: currentDecoration.icon, - color: MobileToolbarTheme.of(context).iconColor, - ), - label: FlowyText(currentDecoration.label), - isSelected: isSelected, - onPressed: () { - if (currentDecoration.name == AppFlowyRichTextKeys.href) { - if (selection.isCollapsed) { - return; - } - - _closeKeyboard(); - - // show edit link bottom sheet - final context = nodes.firstOrNull?.context; - if (context != null) { - final text = editorState - .getTextInSelection( - widget.selection, - ) - .join(); - final href = - editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.href, - widget.selection, - ); - showEditLinkBottomSheet( - context, - text, - href, - (context, newText, newHref) { - _updateTextAndHref(text, href, newText, newHref); - context.pop(); - }, - ); - } - } else { - setState(() { - editorState.toggleAttribute(currentDecoration.name); - }); - } - }, - ); - }) - .nonNulls - .toList(); - - return GridView.count( - shrinkWrap: true, - padding: EdgeInsets.zero, - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 4, - children: children, - ); - } - - void _closeKeyboard() { - editorState.updateSelectionWithReason( - widget.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ); - editorState.service.keyboardService?.closeKeyboard(); - } - - void _updateTextAndHref( - String prevText, - String? prevHref, - String text, - String href, - ) async { - final selection = widget.selection; - if (!selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - final transaction = editorState.transaction; - if (prevText != text) { - transaction.replaceText( - node, - selection.startIndex, - selection.length, - text, - ); - } - // if the text is empty, it means the user wants to remove the text - if (text.isNotEmpty && prevHref != href) { - transaction.formatText(node, selection.startIndex, text.length, { - AppFlowyRichTextKeys.href: href.isEmpty ? null : href, - }); - } - await editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart deleted file mode 100644 index 7fc162868d44..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final redoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.m_redo_m, - ), - actionHandler: (_, editorState) async { - editorState.undoManager.redo(); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart deleted file mode 100644 index 2b22ac1adad4..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final undoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.m_undo_m, - ), - actionHandler: (_, editorState) async { - editorState.undoManager.undo(); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart deleted file mode 100644 index c8609c114e1b..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -class ExportPageWidget extends StatelessWidget { - const ExportPageWidget({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const FlowyText.medium( - 'Open document failed', - fontSize: 18.0, - ), - const VSpace(5), - const FlowyText.regular( - 'Please try to export the page and contact us.', - fontSize: 12.0, - ), - const VSpace(20), - RoundedTextButton( - title: 'Export page', - width: 100, - height: 30, - onPressed: onTap, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/trash/menu.dart b/frontend/appflowy_flutter/lib/plugins/trash/menu.dart deleted file mode 100644 index 414d61d6596e..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/trash/menu.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -class MenuTrash extends StatelessWidget { - const MenuTrash({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greySelect, - ), - isSelected: () => getIt().latestOpenView == null, - child: SizedBox( - height: 26, - child: InkWell( - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - child: _render(context), - ), - ).padding(horizontal: Insets.l), - ).padding(horizontal: 8); - }, - ); - } - - Widget _render(BuildContext context) { - return Row( - children: [ - const FlowySvg( - FlowySvgs.trash_m, - size: Size(16, 16), - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.trash_text.tr()), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart deleted file mode 100644 index 5a7bac2c75e4..000000000000 --- a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:http/http.dart' as http; - -Future isImageExistOnCloud({ - required String url, - required UserProfilePB userProfilePB, -}) async { - final header = {}; - final token = userProfilePB.token; - try { - final decodedToken = jsonDecode(token); - header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; - final response = await http.get(Uri.http(url), headers: header); - return response.statusCode == 200; - } catch (_) { - return false; - } -} diff --git a/frontend/appflowy_flutter/lib/util/base64_string.dart b/frontend/appflowy_flutter/lib/util/base64_string.dart deleted file mode 100644 index 01a7f9a17ed9..000000000000 --- a/frontend/appflowy_flutter/lib/util/base64_string.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'dart:convert'; - -extension Base64Encode on String { - String get base64 => base64Encode(utf8.encode(this)); -} diff --git a/frontend/appflowy_flutter/lib/util/json_print.dart b/frontend/appflowy_flutter/lib/util/json_print.dart deleted file mode 100644 index b73a740249cf..000000000000 --- a/frontend/appflowy_flutter/lib/util/json_print.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; - -const JsonEncoder _encoder = JsonEncoder.withIndent(' '); -void prettyPrintJson(Object? object) { - Log.trace(_encoder.convert(object)); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/home/prelude.dart deleted file mode 100644 index 8b137891791f..000000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/home/prelude.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart deleted file mode 100644 index 715b88f27992..000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart +++ /dev/null @@ -1,19 +0,0 @@ -enum FormFactor { - mobile._(600), - tablet._(840), - desktop._(1280); - - const FormFactor._(this.width); - - factory FormFactor.fromWidth(double width) { - if (width < FormFactor.mobile.width) { - return FormFactor.mobile; - } else if (width < FormFactor.tablet.width) { - return FormFactor.tablet; - } else { - return FormFactor.desktop; - } - } - - final double width; -} From 90058b74218f0f631d8d0334fb148f5dacffd80e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 19 Mar 2024 12:48:27 +0100 Subject: [PATCH 03/16] chore: remove unused elements --- .../notification/document_notification.dart | 8 - .../notification/folder_notification.dart | 6 - .../core/notification/grid_notification.dart | 6 - .../core/notification/user_notification.dart | 35 --- .../cell/bloc/url_cell_editor_bloc.dart | 86 ------ .../application/cell/cell_data_loader.dart | 5 - .../plugins/database/application/defines.dart | 2 - .../application/field/field_controller.dart | 9 +- .../board/application/group_controller.dart | 2 - .../calendar/application/calendar_bloc.dart | 10 - .../application/calendar_setting_bloc.dart | 2 - .../database/domain/group_listener.dart | 2 - .../presentation/widgets/row/mobile_row.dart | 28 +- .../grid/presentation/widgets/shortcuts.dart | 6 - .../widgets/card/container/accessory.dart | 7 +- .../widgets/row/accessory/cell_shortcuts.dart | 18 -- .../database/widgets/row/row_banner.dart | 6 +- .../lib/plugins/document/document_page.dart | 17 -- .../editor_plugins/header/cover_editor.dart | 277 +----------------- .../mobile_add_block_toolbar_item.dart | 3 +- .../lib/user/application/user_listener.dart | 1 - .../workspace/application/home/home_bloc.dart | 13 +- .../workspace/application/view/view_ext.dart | 8 +- .../presentation/home/navigation.dart | 18 +- .../settings/widgets/settings_user_view.dart | 40 +-- 25 files changed, 23 insertions(+), 592 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_editor_bloc.dart diff --git a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart index 4dcaf3fa239a..91743c7f806d 100644 --- a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart @@ -1,14 +1,6 @@ -import 'dart:typed_data'; - import 'package:appflowy/core/notification/notification_helper.dart'; import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef DocumentNotificationCallback = void Function( - DocumentNotification, - FlowyResult, -); class DocumentNotificationParser extends NotificationParser { diff --git a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart index 46cba8cbfe43..ed5890cfefaf 100644 --- a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart @@ -9,12 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; -// Folder -typedef FolderNotificationCallback = void Function( - FolderNotification, - FlowyResult, -); - class FolderNotificationParser extends NotificationParser { FolderNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart index 4d67f0bbb0d2..930e30822e64 100644 --- a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart @@ -9,12 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; -// DatabasePB -typedef DatabaseNotificationCallback = void Function( - DatabaseNotification, - FlowyResult, -); - class DatabaseNotificationParser extends NotificationParser { DatabaseNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart index 741f26967c33..5aafc2a6b106 100644 --- a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart @@ -1,20 +1,8 @@ -import 'dart:async'; -import 'dart:typed_data'; - import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; -// User -typedef UserNotificationCallback = void Function( - UserNotification, - FlowyResult, -); - class UserNotificationParser extends NotificationParser { UserNotificationParser({ @@ -25,26 +13,3 @@ class UserNotificationParser errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } - -typedef UserNotificationHandler = Function( - UserNotification ty, - FlowyResult result, -); - -class UserNotificationListener { - UserNotificationListener({ - required String objectId, - required UserNotificationHandler handler, - }) : _parser = UserNotificationParser(id: objectId, callback: handler) { - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - UserNotificationParser? _parser; - StreamSubscription? _subscription; - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_editor_bloc.dart deleted file mode 100644 index 1ee86a97b704..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_editor_bloc.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'url_cell_editor_bloc.freezed.dart'; - -class URLCellEditorBloc extends Bloc { - URLCellEditorBloc({required this.cellController}) - : super(URLCellEditorState.initial(cellController)) { - _dispatch(); - } - - final URLCellController cellController; - void Function()? _onCellChangedFn; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () { - _startListening(); - }, - updateText: (text) async { - await cellController.saveCellData(text); - emit( - state.copyWith( - content: text, - isFinishEditing: true, - ), - ); - }, - didReceiveCellUpdate: (cellData) { - emit(state.copyWith(content: cellData?.content ?? "")); - }, - ); - }, - ); - } - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellData) { - if (!isClosed) { - add(URLCellEditorEvent.didReceiveCellUpdate(cellData)); - } - }, - ); - } -} - -@freezed -class URLCellEditorEvent with _$URLCellEditorEvent { - const factory URLCellEditorEvent.initial() = _InitialCell; - const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = - _DidReceiveCellUpdate; - const factory URLCellEditorEvent.updateText(String text) = _UpdateText; -} - -@freezed -class URLCellEditorState with _$URLCellEditorState { - const factory URLCellEditorState({ - required String content, - required bool isFinishEditing, - }) = _URLCellEditorState; - - factory URLCellEditorState.initial(URLCellController context) { - final cellData = context.getCellData(); - return URLCellEditorState( - content: cellData?.content ?? "", - isFinishEditing: true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart index c5502bd8b1f0..792f4d5e9d2d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart @@ -6,11 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'cell_controller.dart'; -abstract class IGridCellDataConfig { - // The cell data will reload if it receives the field's change notification. - bool get reloadOnFieldChanged; -} - abstract class CellDataParser { T? parserData(List data); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart index 88e9fc4f77d4..48785fc87fe0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart @@ -31,8 +31,6 @@ typedef OnNumOfRowsChanged = void Function( ChangedReason reason, ); -typedef OnError = void Function(FlowyError); - @freezed class LoadingState with _$LoadingState { const factory LoadingState.idle() = _Idle; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index db1a56071e3d..9778af41e287 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -1,13 +1,15 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; + +import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/application/setting/setting_listener.dart'; import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/domain/field_listener.dart'; import 'package:appflowy/plugins/database/domain/field_settings_listener.dart'; import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; import 'package:appflowy/plugins/database/domain/filter_listener.dart'; import 'package:appflowy/plugins/database/domain/filter_service.dart'; -import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/application/setting/setting_listener.dart'; import 'package:appflowy/plugins/database/domain/sort_listener.dart'; import 'package:appflowy/plugins/database/domain/sort_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; @@ -17,9 +19,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import '../setting/setting_service.dart'; + import 'field_info.dart'; class _GridFieldNotifier extends ChangeNotifier { @@ -72,7 +74,6 @@ typedef OnReceiveUpdateFields = void Function(List); typedef OnReceiveFields = void Function(List); typedef OnReceiveFilters = void Function(List); typedef OnReceiveSorts = void Function(List); -typedef OnReceiveFieldSettings = void Function(List); class FieldController { FieldController({required this.viewId}) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart index 5f1b8c9609fb..7985b92c8fbb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart @@ -9,8 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:protobuf/protobuf.dart'; -typedef OnGroupError = void Function(FlowyError); - abstract class GroupControllerDelegate { bool hasGroup(String groupId); void removeRow(GroupPB group, RowId rowId); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart index 4a2674f1c54d..ecfb4bfff84c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart @@ -456,16 +456,6 @@ class CalendarState with _$CalendarState { ); } -class CalendarEditingRow { - CalendarEditingRow({ - required this.row, - required this.index, - }); - - RowPB row; - int? index; -} - @freezed class CalendarDayEvent with _$CalendarDayEvent { const factory CalendarDayEvent({ diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart index 8ea42d2d3bee..c1288b157dc0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart @@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart'; part 'calendar_setting_bloc.freezed.dart'; -typedef DayOfWeek = int; - class CalendarSettingBloc extends Bloc { CalendarSettingBloc({required DatabaseController databaseController}) diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart index 212bce80c3e3..8720c188664e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart @@ -8,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; -typedef GroupConfigurationUpdateValue - = FlowyResult, FlowyError>; typedef GroupUpdateValue = FlowyResult; typedef GroupByNewFieldValue = FlowyResult, FlowyError>; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart index 92d936010652..b6817fc8487c 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart @@ -1,4 +1,5 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; @@ -8,14 +9,9 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; -import "package:appflowy/generated/locale_keys.g.dart"; -import 'package:easy_localization/easy_localization.dart'; class MobileGridRow extends StatefulWidget { const MobileGridRow({ @@ -90,26 +86,6 @@ class _MobileGridRowState extends State { } } -class InsertRowButton extends StatelessWidget { - const InsertRowButton({super.key}); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - tooltipText: LocaleKeys.tooltip_addNewRow.tr(), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - width: 20, - height: 30, - onPressed: () => context.read().add(const RowEvent.createRow()), - iconPadding: const EdgeInsets.all(3), - icon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).colorScheme.tertiary, - ), - ); - } -} - class RowContent extends StatelessWidget { const RowContent({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart index faf33c61ec3a..adabb7d868a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart @@ -23,12 +23,6 @@ Map bindKeys(List keys) { return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; } -Map> bindActions() { - return { - KeyboardKeyIdent: KeyboardBindingAction(), - }; -} - class KeyboardKeyIdent extends Intent { const KeyboardKeyIdent(this.key); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart index 129e17ca48cc..56045d57d647 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart @@ -1,6 +1,7 @@ -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; + enum AccessoryType { edit, more, @@ -11,10 +12,6 @@ abstract mixin class CardAccessory implements Widget { void onTap(BuildContext context) {} } -typedef CardAccessoryBuilder = List Function( - BuildContext buildContext, -); - class CardAccessoryContainer extends StatelessWidget { const CardAccessoryContainer({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart index 582e5fccd02a..3fc2131f24c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart @@ -96,21 +96,3 @@ class GridCellCopyAction extends Action { } } } - -class GridCellPasteIntent extends Intent { - const GridCellPasteIntent(); -} - -class GridCellPasteAction extends Action { - GridCellPasteAction({required this.child}); - - final CellShortcuts child; - - @override - void invoke(covariant GridCellPasteIntent intent) { - final callback = child.shortcutHandlers[CellKeyboardKey.onInsert]; - if (callback != null) { - callback(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 9f620118e915..46e957ca2dc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -1,21 +1,21 @@ +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/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_popover/appflowy_popover.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'; -typedef OnSubmittedEmoji = void Function(String emoji); const _kBannerActionHeight = 40.0; class RowBanner extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 8cc60eaf7c67..fa848d29ed2b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -180,20 +180,3 @@ class _DocumentPageState extends State { } } } - -class DocumentSyncIndicator extends StatelessWidget { - const DocumentSyncIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.isSyncing) { - return const SizedBox(height: 1, child: LinearProgressIndicator()); - } else { - return const SizedBox(height: 1); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 37753b772476..5c83c1a833d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -1,19 +1,15 @@ import 'dart:io'; import 'dart:ui'; +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/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; const String kLocalImagesKey = 'local_images'; @@ -22,275 +18,6 @@ List get builtInAssetImages => [ "assets/images/app_flowy_abstract_cover_2.jpg", ]; -class ChangeCoverPopover extends StatefulWidget { - const ChangeCoverPopover({ - super.key, - required this.editorState, - required this.node, - required this.onCoverChanged, - }); - - final EditorState editorState; - final Node node; - final Function( - CoverType selectionType, - String selection, - ) onCoverChanged; - - @override - State createState() => _ChangeCoverPopoverState(); -} - -class _ChangeCoverPopoverState extends State { - bool isAddingImage = false; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ChangeCoverPopoverBloc( - editorState: widget.editorState, - node: widget.node, - )..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()), - child: BlocConsumer( - listener: (context, state) { - if (state is Loaded && state.selectLatestImage) { - widget.onCoverChanged( - CoverType.file, - state.imageNames.last, - ); - } - }, - builder: (context, state) { - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: isAddingImage - ? CoverImagePicker( - onBackPressed: () => setState(() { - isAddingImage = false; - }), - onFileSubmit: (_) { - context.read().add( - const ChangeCoverPopoverEvent - .fetchPickedImagePaths( - selectLatestImage: true, - ), - ); - - setState(() => isAddingImage = false); - }, - ) - : _buildCoverSelection(), - ), - ); - }, - ), - ); - } - - Widget _buildCoverSelection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.document_plugins_cover_colors.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - const VSpace(10), - _buildColorPickerList(), - const VSpace(10), - _buildImageHeader(), - const VSpace(10), - _buildFileImagePicker(), - const VSpace(10), - FlowyText.semibold( - LocaleKeys.document_plugins_cover_abstract.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - const VSpace(10), - _buildAbstractImagePicker(), - ], - ); - } - - Widget _buildImageHeader() { - return BlocBuilder( - builder: (context, state) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FlowyText.semibold( - LocaleKeys.document_plugins_cover_images.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - FlowyTextButton( - fillColor: Theme.of(context).cardColor, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - LocaleKeys.document_plugins_cover_clearAll.tr(), - fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async { - final hasFileImageCover = CoverType.fromString( - widget.node.attributes[DocumentHeaderBlockKeys.coverType], - ) == - CoverType.file; - final changeCoverBloc = context.read(); - if (hasFileImageCover) { - await showDialog( - context: context, - builder: (context) { - return DeleteImageAlertDialog( - onSubmit: () { - changeCoverBloc.add( - const ChangeCoverPopoverEvent.clearAllImages(), - ); - Navigator.pop(context); - }, - ); - }, - ); - } else { - context - .read() - .add(const ChangeCoverPopoverEvent.clearAllImages()); - } - }, - mainAxisAlignment: MainAxisAlignment.end, - ), - ], - ); - }, - ); - } - - Widget _buildAbstractImagePicker() { - return GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1 / 0.65, - crossAxisSpacing: 7, - mainAxisSpacing: 7, - ), - itemCount: builtInAssetImages.length, - itemBuilder: (BuildContext ctx, index) { - return InkWell( - onTap: () { - widget.onCoverChanged( - CoverType.asset, - builtInAssetImages[index], - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(builtInAssetImages[index]), - fit: BoxFit.cover, - ), - borderRadius: Corners.s8Border, - ), - ), - ); - }, - ); - } - - Widget _buildColorPickerList() { - final theme = Theme.of(context); - return CoverColorPicker( - pickerBackgroundColor: theme.cardColor, - pickerItemHoverColor: theme.hoverColor, - selectedBackgroundColorHex: - widget.node.attributes[DocumentHeaderBlockKeys.coverType] == - CoverType.color.toString() - ? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails] - : 'ffffff', - backgroundColorOptions: - _generateBackgroundColorOptions(widget.editorState), - onSubmittedBackgroundColorHex: (color) { - widget.onCoverChanged(CoverType.color, color); - setState(() {}); - }, - ); - } - - Widget _buildFileImagePicker() { - return BlocBuilder( - builder: (context, state) { - if (state is Loaded) { - final List images = state.imageNames; - return GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1 / 0.65, - crossAxisSpacing: 7, - mainAxisSpacing: 7, - ), - itemCount: images.length + 1, - itemBuilder: (BuildContext ctx, index) { - if (index == 0) { - return NewCustomCoverButton( - onPressed: () => setState( - () => isAddingImage = true, - ), - ); - } - return ImageGridItem( - onImageSelect: () { - widget.onCoverChanged( - CoverType.file, - images[index - 1], - ); - }, - onImageDelete: () async { - final changeCoverBloc = - context.read(); - final deletingCurrentCover = widget.node - .attributes[DocumentHeaderBlockKeys.coverDetails] == - images[index - 1]; - if (deletingCurrentCover) { - await showDialog( - context: context, - builder: (context) { - return DeleteImageAlertDialog( - onSubmit: () { - changeCoverBloc.add( - ChangeCoverPopoverEvent.deleteImage( - images[index - 1], - ), - ); - Navigator.pop(context); - }, - ); - }, - ); - } else { - changeCoverBloc.add(DeleteImage(images[index - 1])); - } - }, - imagePath: images[index - 1], - ); - }, - ); - } - - return const SizedBox.shrink(); - }, - ); - } - - List _generateBackgroundColorOptions(EditorState editorState) { - return FlowyTint.values - .map( - (t) => ColorOption( - colorHex: t.color(context).toHex(), - name: t.tintName(AppFlowyEditorL10n.current), - ), - ) - .toList(); - } -} - @visibleForTesting class NewCustomCoverButton extends StatelessWidget { const NewCustomCoverButton({super.key, required this.onPressed}); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart index 30d344b04ace..f21abc3dfdf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart @@ -1,10 +1,11 @@ +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/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; // convert the current block to other block types // only show in single selection and text type diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 2fbacf3b6b30..492bf3b35b2b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -15,7 +15,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; typedef UserProfileNotifyValue = FlowyResult; -typedef AuthNotifyValue = FlowyResult; class UserListener { UserListener({ diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 98e8bcf08ecd..f18b05b07166 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -4,8 +4,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' show WorkspaceSettingPB; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flowy_infra/time/duration.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; + part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { @@ -70,17 +70,6 @@ enum MenuResizeType { drag, } -extension MenuResizeTypeExtension on MenuResizeType { - Duration duration() { - switch (this) { - case MenuResizeType.drag: - return 30.milliseconds; - case MenuResizeType.slide: - return 350.milliseconds; - } - } -} - @freezed class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 395699f1472d..7c799a0543ca 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; @@ -11,12 +13,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/material.dart'; - -enum FlowyPlugin { - editor, - kanban, -} class PluginArgumentKeys { static String selection = "selection"; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index 8d5a0259eeec..5d5b465437ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; @@ -9,13 +11,10 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; -typedef NaviAction = void Function(); - class NavigationNotifier with ChangeNotifier { NavigationNotifier({required this.navigationItems}); @@ -146,19 +145,6 @@ class NaviItemWidget extends StatelessWidget { } } -class NaviItemDivider extends StatelessWidget { - const NaviItemDivider({super.key, required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Row( - children: [child, const Text('/')], - ); - } -} - class EllipsisNaviItem extends NavigationItem { EllipsisNaviItem({required this.items}); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 7ef8ccc1dfa1..3f3e516b3a11 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -11,18 +13,15 @@ import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'setting_third_party_login.dart'; -const defaultUserAvatar = '1F600'; const _iconSize = Size(60, 60); class SettingsUserView extends StatelessWidget { @@ -441,41 +440,6 @@ final builtInSVGIcons = [ '1F984', ]; -// REMOVE this widget in next version 0.3.10 -class IconGallery extends StatelessWidget { - const IconGallery({ - super.key, - required this.selectedIcon, - required this.onSelectIcon, - this.defaultOption, - }); - - final String selectedIcon; - final SelectIconCallback onSelectIcon; - final Widget? defaultOption; - - @override - Widget build(BuildContext context) { - return GridView.count( - padding: const EdgeInsets.all(20), - crossAxisCount: 5, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - children: [ - if (defaultOption != null) defaultOption!, - ...builtInSVGIcons.mapIndexed( - (int index, String iconUrl) => IconOption( - emoji: FlowySvgData('emoji/$iconUrl'), - iconUrl: iconUrl, - onSelectIcon: onSelectIcon, - isSelected: iconUrl == selectedIcon, - ), - ), - ], - ); - } -} - class IconOption extends StatelessWidget { IconOption({ required this.emoji, From 9d8d2a2d51fb54b112e0e8f3111aca36d8ff13b7 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 19 Mar 2024 17:36:53 +0100 Subject: [PATCH 04/16] feat: complete account view --- .../settings/pages/settings_account_view.dart | 82 +++++++++++++------ .../shared/settings_alert_dialog.dart | 43 ++++++++++ .../settings/shared/settings_input_field.dart | 24 ++++-- .../resources/flowy_icons/24x/arrow_back.svg | 8 ++ frontend/resources/translations/en.json | 7 +- 5 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 frontend/resources/flowy_icons/24x/arrow_back.svg diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index e8ac3889b9d6..c4e96e90ed8e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -50,6 +50,15 @@ class SettingsAccountView extends StatefulWidget { class _SettingsAccountViewState extends State { late String userName = widget.userProfile.name; + late final TextEditingController _emailController = TextEditingController( + text: widget.userProfile.email, + ); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -57,7 +66,11 @@ class _SettingsAccountViewState extends State { create: (context) => getIt(param1: widget.userProfile) ..add(const SettingsUserEvent.initial()), - child: BlocBuilder( + child: BlocConsumer( + listenWhen: (previous, current) => + previous.userProfile.email != current.userProfile.email, + listener: (context, state) => + _emailController.text = state.userProfile.email, builder: (context, state) { return SingleChildScrollView( physics: const ClampingScrollPhysics(), @@ -100,7 +113,29 @@ class _SettingsAccountViewState extends State { buttonLabel: LocaleKeys .settings_account_email_actions_change .tr(), - onPressed: () {}, + onPressed: () => SettingsAlertDialog( + title: LocaleKeys + .settings_account_email_actions_change + .tr(), + confirmLabel: LocaleKeys.button_save.tr(), + confirm: () { + context.read().add( + SettingsUserEvent.updateUserEmail( + _emailController.text, + ), + ); + Navigator.of(context).pop(); + }, + children: [ + SettingsInputField( + label: + LocaleKeys.settings_account_email_title.tr(), + value: state.userProfile.email, + hideActions: true, + textController: _emailController, + ), + ], + ).show(context), ), ], ), @@ -131,6 +166,8 @@ class _SettingsAccountViewState extends State { label: LocaleKeys.settings_account_keys_openAILabel.tr(), tooltip: LocaleKeys.settings_account_keys_openAITooltip.tr(), + placeholder: + LocaleKeys.settings_account_keys_openAIHint.tr(), value: state.userProfile.openaiKey, obscureText: true, onSave: (key) => context @@ -210,16 +247,11 @@ class _SignInButton extends StatelessWidget { fillColor: Theme.of(context).colorScheme.primary, hoverColor: const Color(0xFF005483), fontHoverColor: Colors.white, - onPressed: () { - SettingsAlertDialog( - title: LocaleKeys.settings_account_login_loginLabel.tr(), - children: [ - SettingThirdPartyLogin( - didLogin: didLogin, - ), - ], - ).show(context); - }, + onPressed: () => SettingsAlertDialog( + title: LocaleKeys.settings_account_login_loginLabel.tr(), + implyLeading: true, + children: [SettingThirdPartyLogin(didLogin: didLogin)], + ).show(context), ), ), ], @@ -254,20 +286,18 @@ class _SignOutButton extends StatelessWidget { fillColor: Theme.of(context).colorScheme.primary, hoverColor: const Color(0xFF005483), fontHoverColor: Colors.white, - onPressed: () { - SettingsAlertDialog( - title: LocaleKeys.settings_account_login_logoutLabel, - subtitle: switch (userProfile.encryptionType) { - EncryptionTypePB.Symmetric => - LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(), - _ => LocaleKeys.settings_menu_logoutPrompt.tr(), - }, - confirm: () async { - await getIt().signOut(); - didLogout(); - }, - ).show(context); - }, + onPressed: () => SettingsAlertDialog( + title: LocaleKeys.settings_account_login_logoutLabel.tr(), + subtitle: switch (userProfile.encryptionType) { + EncryptionTypePB.Symmetric => + LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(), + _ => LocaleKeys.settings_menu_logoutPrompt.tr(), + }, + confirm: () async { + await getIt().signOut(); + didLogout(); + }, + ).show(context), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart index 48d788186e67..bc3c7a8f2139 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -17,6 +18,7 @@ class SettingsAlertDialog extends StatefulWidget { this.confirmLabel, this.hideCancelButton = false, this.isDangerous = false, + this.implyLeading = false, }); final String title; @@ -28,6 +30,9 @@ class SettingsAlertDialog extends StatefulWidget { final bool hideCancelButton; final bool isDangerous; + /// If true, a back button will show in the top left corner + final bool implyLeading; + @override State createState() => _SettingsAlertDialogState(); } @@ -43,6 +48,44 @@ class _SettingsAlertDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.implyLeading) ...[ + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + children: [ + const FlowySvg( + FlowySvgs.arrow_back_m, + size: Size.square(24), + ), + const HSpace(8), + FlowyText.semibold( + LocaleKeys.button_back.tr(), + fontSize: 16, + ), + ], + ), + ), + ), + ], + const Spacer(), + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowySvg( + FlowySvgs.m_close_m, + size: const Size.square(20), + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart index 3841466920da..bb8cfd93978f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart @@ -18,6 +18,7 @@ class SettingsInputField extends StatefulWidget { const SettingsInputField({ super.key, this.label, + this.textController, this.focusNode, this.obscureText = false, this.value, @@ -25,10 +26,13 @@ class SettingsInputField extends StatefulWidget { this.tooltip, this.onSave, this.onCancel, + this.hideActions = false, }); final String? label; + final TextEditingController? textController; + final FocusNode? focusNode; /// If true, the input field will be obscured @@ -40,6 +44,11 @@ class SettingsInputField extends StatefulWidget { final String? placeholder; final String? tooltip; + /// If true the save and cancel options will not show below the + /// input field. + /// + final bool hideActions; + final Function(String)? onSave; /// The action to be performed when the cancel button is pressed. @@ -54,16 +63,19 @@ class SettingsInputField extends StatefulWidget { } class _SettingsInputFieldState extends State { - late final controller = TextEditingController(text: widget.value); - late bool obscureText = widget.obscureText; + late final controller = + widget.textController ?? TextEditingController(text: widget.value); late final FocusNode focusNode = widget.focusNode ?? FocusNode(); + late bool obscureText = widget.obscureText; @override void dispose() { if (widget.focusNode == null) { focusNode.dispose(); } - controller.dispose(); + if (widget.textController == null) { + controller.dispose(); + } super.dispose(); } @@ -95,6 +107,7 @@ class _SettingsInputFieldState extends State { height: 48, child: FlowyTextField( focusNode: focusNode, + hintText: widget.placeholder, controller: controller, autoFocus: false, obscureText: obscureText, @@ -120,8 +133,9 @@ class _SettingsInputFieldState extends State { onChanged: (_) => setState(() {}), ), ), - if ((widget.value == null && controller.text.isNotEmpty) || - widget.value != null && widget.value != controller.text) ...[ + if (!widget.hideActions && + ((widget.value == null && controller.text.isNotEmpty) || + widget.value != null && widget.value != controller.text)) ...[ const VSpace(8), Row( children: [ diff --git a/frontend/resources/flowy_icons/24x/arrow_back.svg b/frontend/resources/flowy_icons/24x/arrow_back.svg new file mode 100644 index 000000000000..ff56023329f3 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/arrow_back.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f3d37ba28601..ba7cee5408a0 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -304,12 +304,13 @@ "keys": { "title": "AI Keys", "openAILabel": "OpenAI key", - "openAITooltip": "The OpenAPI API key to use for the AI models" + "openAITooltip": "The OpenAPI API key to use for the AI models", + "openAIHint": "Input your OpenAI API Key" }, "login": { "title": "Account login", - "loginLabel": "Log in", - "logoutLabel": "Log out" + "loginLabel": "Login", + "logoutLabel": "Logout" } }, "menu": { From 24f5f9944650916a5a6100df0168ef0a396d8417 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Thu, 21 Mar 2024 05:01:13 +0100 Subject: [PATCH 05/16] feat: workspace settings --- .../settings/settings_dialog_bloc.dart | 1 + .../workspace/workspace_settings_bloc.dart | 114 ++++++++ .../settings/pages/settings_account_view.dart | 254 +++++++++--------- .../pages/settings_workspace_view.dart | 137 ++++++++++ .../settings/settings_dialog.dart | 3 + .../shared/settings_actionable_input.dart | 51 ++++ .../settings/shared/settings_body.dart | 22 ++ .../settings/shared/settings_category.dart | 11 + .../settings/widgets/settings_menu.dart | 7 + frontend/resources/translations/en.json | 3 + .../rust-lib/flowy-user/src/event_handler.rs | 15 ++ frontend/rust-lib/flowy-user/src/event_map.rs | 4 + 12 files changed, 491 insertions(+), 131 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 382b3441e7f8..b29ab29766a3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -10,6 +10,7 @@ part 'settings_dialog_bloc.freezed.dart'; enum SettingsPage { account, + workspace, // OLD appearance, language, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart new file mode 100644 index 000000000000..d1bd7a8319e3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -0,0 +1,114 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'workspace_settings_bloc.freezed.dart'; + +class WorkspaceSettingsBloc + extends Bloc { + WorkspaceSettingsBloc() : super(WorkspaceSettingsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: (userProfile, workspace) async { + UserWorkspacePB? currentWorkspace = workspace; + if (workspace == null) { + currentWorkspace = await _getWorkspace(userProfile.workspaceId); + } + + // We emit here because the next event might take longer. + emit(state.copyWith(workspace: currentWorkspace!)); + + final members = await _getWorkspaceMembers(userProfile.workspaceId); + final role = members + .firstWhereOrNull((e) => e.email == userProfile.email) + ?.role ?? + AFRolePB.Guest; + + emit(state.copyWith(members: members, myRole: role)); + }, + updateWorkspaceName: (name) async { + final request = RenameWorkspacePB( + workspaceId: state.workspace?.workspaceId, + newName: name, + ); + final workspace = await UserEventRenameWorkspace(request).send(); + + workspace.fold( + (_) => emit( + state.copyWith(workspace: state.workspace?..name = name), + ), + (e) => Log.error('Failed to rename workspace: $e'), + ); + }, + addWorkspaceMember: (email) {}, + removeWorkspaceMember: (email) {}, + updateWorkspaceMember: (email, role) {}, + ); + }, + ); + } + + Future _getWorkspace(String workspaceId) async { + final request = UserWorkspaceIdPB(workspaceId: workspaceId); + final result = await UserEventGetWorkspace(request).send(); + return result.fold( + (workspace) => workspace, + (e) { + Log.error('Failed to read workspace: $e'); + return UserWorkspacePB(); + }, + ); + } + + Future> _getWorkspaceMembers( + String workspaceId, + ) async { + final data = QueryWorkspacePB()..workspaceId = workspaceId; + final result = await UserEventGetWorkspaceMember(data).send(); + return result.fold( + (s) => s.items, + (e) { + Log.error('Failed to read workspace members: $e'); + return []; + }, + ); + } +} + +@freezed +class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent { + const factory WorkspaceSettingsEvent.initial({ + required UserProfilePB userProfile, + @Default(null) UserWorkspacePB? workspace, + }) = Initial; + + // Workspace itself + const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) = + UpdateWorkspaceName; + + // Workspace Member + const factory WorkspaceSettingsEvent.addWorkspaceMember(String email) = + AddWorkspaceMember; + const factory WorkspaceSettingsEvent.removeWorkspaceMember(String email) = + RemoveWorkspaceMember; + const factory WorkspaceSettingsEvent.updateWorkspaceMember( + String email, + AFRolePB role, + ) = UpdateWorkspaceMember; +} + +@freezed +class WorkspaceSettingsState with _$WorkspaceSettingsState { + const factory WorkspaceSettingsState({ + @Default(null) UserWorkspacePB? workspace, + @Default([]) List members, + @Default(AFRolePB.Guest) AFRolePB myRole, + }) = _WorkspaceSettingsState; + + factory WorkspaceSettingsState.initial() => const WorkspaceSettingsState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index c4e96e90ed8e..229735c6b261 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -9,6 +9,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; @@ -72,147 +73,138 @@ class _SettingsAccountViewState extends State { listener: (context, state) => _emailController.text = state.userProfile.email, builder: (context, state) { - return SingleChildScrollView( - physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsHeader( - title: LocaleKeys.settings_account_title.tr(), - description: LocaleKeys.settings_account_description.tr(), - ), - SettingsCategory( - title: LocaleKeys.settings_account_general_title.tr(), - children: [ - _UserProfileSetting( - name: userName, - iconUrl: state.userProfile.iconUrl, - onSave: (newName) { - // Pseudo change the name to update the UI before the backend - // processes the request. This is to give the user a sense of - // immediate feedback, and avoid UI flickering. - setState(() => userName = newName); - context - .read() - .add(SettingsUserEvent.updateUserName(newName)); - }, - ), - ], - ), - // Only show change email if the user is authenticated and not using local auth - if (isAuthEnabled && - state.userProfile.authenticator != - AuthenticatorPB.Local) ...[ - const SettingsCategorySpacer(), - SettingsCategory( - title: LocaleKeys.settings_account_email_title.tr(), - children: [ - SingleSettingAction( - label: state.userProfile.email, - buttonLabel: LocaleKeys - .settings_account_email_actions_change - .tr(), - onPressed: () => SettingsAlertDialog( - title: LocaleKeys - .settings_account_email_actions_change - .tr(), - confirmLabel: LocaleKeys.button_save.tr(), - confirm: () { - context.read().add( - SettingsUserEvent.updateUserEmail( - _emailController.text, - ), - ); - Navigator.of(context).pop(); - }, - children: [ - SettingsInputField( - label: - LocaleKeys.settings_account_email_title.tr(), - value: state.userProfile.email, - hideActions: true, - textController: _emailController, - ), - ], - ).show(context), - ), - ], + return SettingsBody( + children: [ + SettingsHeader( + title: LocaleKeys.settings_account_title.tr(), + description: LocaleKeys.settings_account_description.tr(), + ), + SettingsCategory( + title: LocaleKeys.settings_account_general_title.tr(), + children: [ + _UserProfileSetting( + name: userName, + iconUrl: state.userProfile.iconUrl, + onSave: (newName) { + // Pseudo change the name to update the UI before the backend + // processes the request. This is to give the user a sense of + // immediate feedback, and avoid UI flickering. + setState(() => userName = newName); + context + .read() + .add(SettingsUserEvent.updateUserName(newName)); + }, ), ], - - /// TODO: Uncomment and finish implementation when we have the feature - // const SettingsCategorySpacer(), - // SettingsCategory( - // title: 'Account & security', - // children: [ - // SingleSettingAction( - // label: '**********', - // buttonLabel: 'Change password', - // onPressed: () {}, - // ), - // SingleSettingAction( - // label: '2-step authentication', - // buttonLabel: 'Enable 2FA', - // onPressed: () {}, - // ), - // ], - // ), + ), + // Only show change email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ const SettingsCategorySpacer(), SettingsCategory( - title: LocaleKeys.settings_account_keys_title.tr(), + title: LocaleKeys.settings_account_email_title.tr(), children: [ - SettingsInputField( - label: LocaleKeys.settings_account_keys_openAILabel.tr(), - tooltip: - LocaleKeys.settings_account_keys_openAITooltip.tr(), - placeholder: - LocaleKeys.settings_account_keys_openAIHint.tr(), - value: state.userProfile.openaiKey, - obscureText: true, - onSave: (key) => context - .read() - .add(SettingsUserEvent.updateUserOpenAIKey(key)), + SingleSettingAction( + label: state.userProfile.email, + buttonLabel: + LocaleKeys.settings_account_email_actions_change.tr(), + onPressed: () => SettingsAlertDialog( + title: LocaleKeys.settings_account_email_actions_change + .tr(), + confirmLabel: LocaleKeys.button_save.tr(), + confirm: () { + context.read().add( + SettingsUserEvent.updateUserEmail( + _emailController.text, + ), + ); + Navigator.of(context).pop(); + }, + children: [ + SettingsInputField( + label: LocaleKeys.settings_account_email_title.tr(), + value: state.userProfile.email, + hideActions: true, + textController: _emailController, + ), + ], + ).show(context), ), ], ), - const SettingsCategorySpacer(), - SettingsCategory( - title: LocaleKeys.settings_account_login_title.tr(), - children: [ - if (state.userProfile.authenticator == - AuthenticatorPB.Local) ...[ - _SignInButton( - userProfile: state.userProfile, - didLogin: widget.didLogin, - ), - ] else ...[ - _SignOutButton( - userProfile: state.userProfile, - didLogout: widget.didLogout, - ), - ], + ], + + /// TODO: Uncomment and finish implementation when we have the feature + // const SettingsCategorySpacer(), + // SettingsCategory( + // title: 'Account & security', + // children: [ + // SingleSettingAction( + // label: '**********', + // buttonLabel: 'Change password', + // onPressed: () {}, + // ), + // SingleSettingAction( + // label: '2-step authentication', + // buttonLabel: 'Enable 2FA', + // onPressed: () {}, + // ), + // ], + // ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_account_keys_title.tr(), + children: [ + SettingsInputField( + label: LocaleKeys.settings_account_keys_openAILabel.tr(), + tooltip: + LocaleKeys.settings_account_keys_openAITooltip.tr(), + placeholder: + LocaleKeys.settings_account_keys_openAIHint.tr(), + value: state.userProfile.openaiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(key)), + ), + ], + ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_account_login_title.tr(), + children: [ + if (state.userProfile.authenticator == + AuthenticatorPB.Local) ...[ + _SignInButton( + userProfile: state.userProfile, + didLogin: widget.didLogin, + ), + ] else ...[ + _SignOutButton( + userProfile: state.userProfile, + didLogout: widget.didLogout, + ), ], - ), + ], + ), - /// TODO: Uncomment and finish implementation when we have the feature - // const SettingsCategorySpacer(), - // SettingsSubcategory( - // title: 'Delete account', - // children: [ - // SingleSettingAction( - // label: - // 'Permanently delete your account and remove access from all teamspaces.', - // labelMaxLines: 4, - // onPressed: () {}, - // buttonLabel: 'Delete my account', - // isDangerous: true, - // fontSize: 12, - // ), - // ], - // ), - ], - ), + /// TODO: Uncomment and finish implementation when we have the feature + // const SettingsCategorySpacer(), + // SettingsSubcategory( + // title: 'Delete account', + // children: [ + // SingleSettingAction( + // label: + // 'Permanently delete your account and remove access from all teamspaces.', + // labelMaxLines: 4, + // onPressed: () {}, + // buttonLabel: 'Delete my account', + // isDangerous: true, + // fontSize: 12, + // ), + // ], + // ), + ], ); }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart new file mode 100644 index 000000000000..0c3c0edfcc85 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsWorkspaceView extends StatefulWidget { + const SettingsWorkspaceView({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => _SettingsWorkspaceViewState(); +} + +class _SettingsWorkspaceViewState extends State { + final TextEditingController _workspaceNameController = + TextEditingController(); + + @override + void dispose() { + _workspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WorkspaceSettingsBloc() + ..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.workspace?.name != current.workspace?.name, + listener: (context, state) => + _workspaceNameController.text = state.workspace?.name ?? '', + builder: (context, state) { + return SettingsBody( + children: [ + const SettingsHeader( + title: 'Workspace', + description: + 'Customize your workspace appearance, theme, font, text layout, date, time, and language.', + ), + // We don't allow changing workspace name for local/offline + if (widget.userProfile.authenticator != + AuthenticatorPB.Local) ...[ + SettingsCategory( + title: 'Workspace name', + children: [ + SettingsActionableInput( + controller: _workspaceNameController, + actions: [ + SizedBox( + height: 48, + child: FlowyTextButton( + LocaleKeys.button_save.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () => context + .read() + .add( + WorkspaceSettingsEvent.updateWorkspaceName( + _workspaceNameController.text, + ), + ), + ), + ), + ], + ), + ], + ), + ], + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Workspace icon', + description: + 'Customize your workspace appearance, theme, font, text layout, date, time, and language.', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Appearance', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Theme', + description: + 'Select a preset theme, or upload your own custom theme.', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Workspace font', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Text direction', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Date & time', + children: [], + ), + const SettingsCategorySpacer(), + const SettingsCategory( + title: 'Language', + children: [], + ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index dbc7654445cc..d08094dba69a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -66,6 +67,8 @@ class SettingsDialog extends StatelessWidget { didLogout: didLogout, didLogin: dismissDialog, ); + case SettingsPage.workspace: + return SettingsWorkspaceView(userProfile: user); // case SettingsPage.language: // return const SettingsLanguageView(); // case SettingsPage.files: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart new file mode 100644 index 000000000000..12abb1699be9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class SettingsActionableInput extends StatelessWidget { + const SettingsActionableInput({ + super.key, + required this.controller, + this.focusNode, + this.placeholder, + this.onSave, + this.actions = const [], + }); + + final TextEditingController controller; + final FocusNode? focusNode; + final String? placeholder; + final Function(String)? onSave; + + final List actions; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: SizedBox( + height: 48, + child: FlowyTextField( + controller: controller, + focusNode: focusNode, + hintText: placeholder, + autoFocus: false, + isDense: false, + suffixIconConstraints: + BoxConstraints.tight(const Size(23 + 18, 24)), + onSubmitted: onSave, + ), + ), + ), + if (actions.isNotEmpty) ...[ + const HSpace(8), + SeparatedRow( + separatorBuilder: () => const HSpace(16), + children: actions, + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart new file mode 100644 index 000000000000..b1ade9c118c7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class SettingsBody extends StatelessWidget { + const SettingsBody({ + super.key, + required this.children, + }); + + final List children; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index b8fb8c38dc82..3dfc34828a04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -9,10 +9,12 @@ class SettingsCategory extends StatelessWidget { const SettingsCategory({ super.key, required this.title, + this.description, required this.children, }); final String title; + final String? description; final List children; @@ -28,6 +30,15 @@ class SettingsCategory extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const VSpace(8), + if (description?.isNotEmpty ?? false) ...[ + FlowyText.regular( + description!, + maxLines: 4, + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + const VSpace(8), + ], SeparatedColumn( mainAxisSize: MainAxisSize.min, separatorBuilder: () => diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 0776766e3b30..89ac59820cde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -48,6 +48,13 @@ class SettingsMenu extends StatelessWidget { icon: FlowySvgs.settings_account_m, changeSelectedPage: changeSelectedPage, ), + SettingsMenuElement( + page: SettingsPage.workspace, + selectedPage: currentPage, + label: LocaleKeys.settings_workspace_menuLabel.tr(), + icon: FlowySvgs.settings_workplace_m, + changeSelectedPage: changeSelectedPage, + ), // SettingsMenuElement( // page: SettingsPage.appearance, // selectedPage: currentPage, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ba7cee5408a0..147354e1aa88 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -313,6 +313,9 @@ "logoutLabel": "Logout" } }, + "workspace": { + "menuLabel": "Workspace" + }, "menu": { "appearance": "Appearance", "language": "Language", diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index bff1ef891b9f..22d95478039e 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -479,6 +479,21 @@ pub async fn get_all_workspace_handler( data_result_ok(user_workspaces.into()) } +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub async fn get_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let workspace_id = data.into_inner().workspace_id; + let uid = manager.get_session()?.user_id; + let workspace = manager.get_user_workspace(uid, &workspace_id).ok_or( + FlowyError::internal() + .with_context("Failed to find a Workspace with id: ".to_owned() + &workspace_id), + )?; + data_result_ok(workspace.into()) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn open_workspace_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 611fd9bad1fa..c3c5d3fbf023 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -60,6 +60,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::UpdateWorkspaceMember, update_workspace_member_handler) // Workspace .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) + .event(UserEvent::GetWorkspace, get_workspace_handler) .event(UserEvent::CreateWorkspace, create_workspace_handler) .event(UserEvent::DeleteWorkspace, delete_workspace_handler) .event(UserEvent::RenameWorkspace, rename_workspace_handler) @@ -135,6 +136,9 @@ pub enum UserEvent { #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, + #[event(input = "UserWorkspaceIdPB", output = "UserWorkspacePB")] + GetWorkspace = 18, + #[event(input = "UserWorkspaceIdPB")] OpenWorkspace = 21, From 48ca562aab7065037a97219424b2ba3398b998e5 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Fri, 22 Mar 2024 12:04:49 +0100 Subject: [PATCH 06/16] fix: dialog min max width --- .../lib/workspace/presentation/settings/settings_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index d08094dba69a..3d28c6b2b413 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -29,6 +29,7 @@ class SettingsDialog extends StatelessWidget { ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => FlowyDialog( + constraints: const BoxConstraints(minWidth: 564, maxHeight: 784), child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, From e9fb1615ae45c617f738b2326f117913b92b8ca2 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 24 Mar 2024 15:57:56 +0100 Subject: [PATCH 07/16] chore: localizations for workspace --- .../pages/settings_workspace_view.dart | 44 +++++++++---------- frontend/resources/translations/en.json | 33 +++++++++++++- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 0c3c0edfcc85..f253ff2fb15a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -48,16 +48,15 @@ class _SettingsWorkspaceViewState extends State { builder: (context, state) { return SettingsBody( children: [ - const SettingsHeader( - title: 'Workspace', - description: - 'Customize your workspace appearance, theme, font, text layout, date, time, and language.', + SettingsHeader( + title: LocaleKeys.settings_workspace_title.tr(), + description: LocaleKeys.settings_workspace_description.tr(), ), // We don't allow changing workspace name for local/offline if (widget.userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( - title: 'Workspace name', + title: LocaleKeys.settings_workspace_workspaceName_title.tr(), children: [ SettingsActionableInput( controller: _workspaceNameController, @@ -90,42 +89,43 @@ class _SettingsWorkspaceViewState extends State { ), ], const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Workspace icon', - description: - 'Customize your workspace appearance, theme, font, text layout, date, time, and language.', + SettingsCategory( + title: LocaleKeys.settings_workspace_workspaceIcon_title.tr(), + description: LocaleKeys + .settings_workspace_workspaceIcon_description + .tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Appearance', + SettingsCategory( + title: LocaleKeys.settings_workspace_appearance_title.tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Theme', + SettingsCategory( + title: LocaleKeys.settings_workspace_theme_title.tr(), description: - 'Select a preset theme, or upload your own custom theme.', + LocaleKeys.settings_workspace_theme_description.tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Workspace font', + SettingsCategory( + title: LocaleKeys.settings_workspace_workspaceFont_title.tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Text direction', + SettingsCategory( + title: LocaleKeys.settings_workspace_textDirection_title.tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Date & time', + SettingsCategory( + title: LocaleKeys.settings_workspace_dateTime_title.tr(), children: [], ), const SettingsCategorySpacer(), - const SettingsCategory( - title: 'Language', + SettingsCategory( + title: LocaleKeys.settings_workspace_language_title.tr(), children: [], ), ], diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b4eb2e9d186a..328b53cc730a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -324,7 +324,36 @@ } }, "workspace": { - "menuLabel": "Workspace" + "menuLabel": "Workspace", + "title": "Workspace", + "description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.", + "workspaceName": { + "title": "Workspace name" + }, + "workspaceIcon": { + "title": "Workspace icon", + "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language." + }, + "appearance": { + "title": "Appearance", + "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language." + }, + "theme": { + "title": "Theme", + "description": "Select a preset theme, or upload your own custom theme." + }, + "workspaceFont": { + "title": "Workspace font" + }, + "textDirection": { + "title": "Text direction" + }, + "dateTime": { + "title": "Date & time" + }, + "language": { + "title": "Language" + } }, "menu": { "appearance": "Appearance", @@ -1467,4 +1496,4 @@ "noNetworkConnected": "No network connected" } } -} \ No newline at end of file +} From 814d3f837648b2ebf46a5486e0ab502290cc51cf Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Fri, 29 Mar 2024 11:05:56 +0100 Subject: [PATCH 08/16] feat: update workspace icon --- .../home/mobile_home_page_header.dart | 9 ++- .../workspace/workspace_settings_bloc.dart | 34 ++++++++++- .../home/menu/sidebar/sidebar_workspace.dart | 9 ++- .../workspace/_sidebar_workspace_icon.dart | 16 ++--- .../workspace/_sidebar_workspace_menu.dart | 31 +++++++--- .../pages/settings_workspace_view.dart | 58 ++++++++++++++----- .../settings/settings_dialog.dart | 5 +- .../settings/widgets/settings_menu.dart | 19 +++--- 8 files changed, 133 insertions(+), 48 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index e8aa0d2b266e..b77b4a317dfe 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; @@ -11,7 +13,6 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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'; @@ -117,6 +118,12 @@ class _MobileWorkspace extends StatelessWidget { workspace: currentWorkspace, iconSize: 26, enableEdit: false, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + currentWorkspace.workspaceId, + result.emoji, + ), + ), ), ), const HSpace(8), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index d1bd7a8319e3..d783fbde6d4a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'workspace_settings_bloc.freezed.dart'; @@ -36,15 +37,41 @@ class WorkspaceSettingsBloc workspaceId: state.workspace?.workspaceId, newName: name, ); - final workspace = await UserEventRenameWorkspace(request).send(); + final result = await UserEventRenameWorkspace(request).send(); - workspace.fold( + result.fold( (_) => emit( state.copyWith(workspace: state.workspace?..name = name), ), (e) => Log.error('Failed to rename workspace: $e'), ); }, + updateWorkspaceIcon: (icon) async { + if (state.workspace == null) { + return null; + } + + final request = ChangeWorkspaceIconPB() + ..workspaceId = state.workspace!.workspaceId + ..newIcon = icon; + final result = await UserEventChangeWorkspaceIcon(request).send(); + + result.fold( + (_) { + final workspace = state.workspace?..freeze(); + if (workspace != null) { + final newWorkspace = workspace.rebuild((p0) { + p0.icon = icon; + }); + + return emit(state.copyWith(workspace: newWorkspace)); + } + + Log.error('Failed to update workspace icon, no workspace.'); + }, + (e) => Log.error('Failed to update workspace icon: $e'), + ); + }, addWorkspaceMember: (email) {}, removeWorkspaceMember: (email) {}, updateWorkspaceMember: (email, role) {}, @@ -91,6 +118,9 @@ class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent { const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) = UpdateWorkspaceName; + const factory WorkspaceSettingsEvent.updateWorkspaceIcon(String icon) = + UpdateWorkspaceIcon; + // Workspace Member const factory WorkspaceSettingsEvent.addWorkspaceMember(String email) = AddWorkspaceMember; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart index f75dcb91db3a..65fe815ce713 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; @@ -13,7 +15,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_popover/appflowy_popover.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 SidebarWorkspace extends StatelessWidget { @@ -179,6 +180,12 @@ class _SidebarSwitchWorkspaceButtonState workspace: widget.currentWorkspace, iconSize: 18, enableEdit: false, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + widget.currentWorkspace.workspaceId, + result.emoji, + ), + ), ), ), const HSpace(4), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index ebe53420a5d1..ce88180ee447 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,23 +1,24 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ super.key, + required this.workspace, required this.enableEdit, required this.iconSize, - required this.workspace, + required this.onSelected, }); final UserWorkspacePB workspace; final double iconSize; final bool enableEdit; + final void Function(EmojiPickerResult) onSelected; @override State createState() => _WorkspaceIconState(); @@ -60,12 +61,7 @@ class _WorkspaceIconState extends State { popupBuilder: (BuildContext popoverContext) { return FlowyIconPicker( onSelected: (result) { - context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - widget.workspace.workspaceId, - result.emoji, - ), - ); + widget.onSelected(result); controller.close(); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 9387aea155ba..cc4e1f23061f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; @@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.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'; @visibleForTesting @@ -175,13 +176,24 @@ class WorkspaceMenuItem extends StatelessWidget { workspace: workspace, iconSize: 26, enableEdit: true, + onSelected: (result) => + context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + workspace.workspaceId, + result.emoji, + ), + ), ), ), ), Positioned( right: 12.0, child: Align( - child: _buildRightIcon(context), + child: _buildRightIcon( + context, + state.isLoading, + state.myRole.isOwner, + ), ), ), ], @@ -192,20 +204,21 @@ class WorkspaceMenuItem extends StatelessWidget { ); } - Widget _buildRightIcon(BuildContext context) { + Widget _buildRightIcon( + BuildContext context, + bool isLoading, + bool isOwner, + ) { // only the owner can update or delete workspace. // only show the more action button when the workspace is selected. - if (!isSelected || context.read().state.isLoading) { + if (!isSelected || isLoading) { return const SizedBox.shrink(); } return Row( children: [ - if (context.read().state.myRole.isOwner) - WorkspaceMoreActionList(workspace: workspace), - const FlowySvg( - FlowySvgs.blue_check_s, - ), + if (isOwner) WorkspaceMoreActionList(workspace: workspace), + const FlowySvg(FlowySvgs.blue_check_s), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index f253ff2fb15a..e35c3f703ae3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; @@ -88,45 +89,74 @@ class _SettingsWorkspaceViewState extends State { ], ), ], - const SettingsCategorySpacer(), - SettingsCategory( - title: LocaleKeys.settings_workspace_workspaceIcon_title.tr(), - description: LocaleKeys - .settings_workspace_workspaceIcon_description - .tr(), - children: [], - ), + if (state.workspace != null) ...[ + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_workspace_workspaceIcon_title.tr(), + description: LocaleKeys + .settings_workspace_workspaceIcon_description + .tr(), + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + height: 64, + width: 64, + child: Padding( + padding: const EdgeInsets.all(1), + child: WorkspaceIcon( + workspace: state.workspace!, + iconSize: state.workspace?.icon.isNotEmpty == true + ? 46 + : 20, + enableEdit: true, + onSelected: (emojiResult) => + context.read().add( + WorkspaceSettingsEvent.updateWorkspaceIcon( + emojiResult.emoji, + ), + ), + ), + ), + ), + ], + ), + ], const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_appearance_title.tr(), - children: [], + children: const [], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_theme_title.tr(), description: LocaleKeys.settings_workspace_theme_description.tr(), - children: [], + children: const [], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_workspaceFont_title.tr(), - children: [], + children: const [], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_textDirection_title.tr(), - children: [], + children: const [], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_dateTime_title.tr(), - children: [], + children: const [], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_language_title.tr(), - children: [], + children: const [], ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 3d28c6b2b413..1b5583deaed7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -4,6 +4,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -91,8 +92,8 @@ class SettingsDialog extends StatelessWidget { // return const SettingsCustomizeShortcutsWrapper(); // case SettingsPage.member: // return WorkspaceMembersPage(userProfile: user); - // case SettingsPage.featureFlags: - // return const FeatureFlagsPage(); + case SettingsPage.featureFlags: + return const FeatureFlagsPage(); default: return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 083881df07d6..6de693f97bda 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -112,15 +113,15 @@ class SettingsMenu extends StatelessWidget { // icon: Icons.people, // changeSelectedPage: changeSelectedPage, // ), - // if (kDebugMode) - // SettingsMenuElement( - // // no need to translate this page - // page: SettingsPage.featureFlags, - // selectedPage: currentPage, - // label: 'Feature Flags', - // icon: Icons.flag, - // changeSelectedPage: changeSelectedPage, - // ), + if (kDebugMode) + SettingsMenuElement( + // no need to translate this page + page: SettingsPage.featureFlags, + selectedPage: currentPage, + label: 'Feature Flags', + icon: FlowySvgs.folder_m, + changeSelectedPage: changeSelectedPage, + ), ], ), ), From 1a2b32d28a1a856e0c908cc83f840a357703929a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Fri, 29 Mar 2024 11:31:57 +0100 Subject: [PATCH 09/16] fix: fixes after merging main --- .../workspace_menu_bottom_sheet.dart | 10 ++++++++- .../workspace/_sidebar_workspace_icon.dart | 21 ++++++------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index a41e8aba9702..8116cc51bc90 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -1,12 +1,14 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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'; // Only works on mobile. @@ -103,6 +105,12 @@ class _WorkspaceMenuItem extends StatelessWidget { enableEdit: false, iconSize: 26, workspace: workspace, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + workspace.workspaceId, + result.emoji, + ), + ), ), trailing: workspace.workspaceId == currentWorkspace.workspaceId ? const FlowySvg( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 02b0480ddbad..1daac0d4ffda 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ @@ -66,19 +64,12 @@ class _WorkspaceIconState extends State { direction: PopoverDirection.bottomWithLeftAligned, constraints: BoxConstraints.loose(const Size(360, 380)), clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (BuildContext popoverContext) { - return FlowyIconPicker( - onSelected: (result) { - context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - widget.workspace.workspaceId, - result.emoji, - ), - ); - controller.close(); - }, - ); - }, + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) { + widget.onSelected(result); + controller.close(); + }, + ), child: MouseRegion( cursor: SystemMouseCursors.click, child: child, From 90ca1a4ae67c6cd61c776a01ecacc90491d5635e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 2 Apr 2024 11:55:20 +0200 Subject: [PATCH 10/16] fix: instant feedback on collapse menu buttons --- .../lib/core/frameless_window.dart | 9 +++---- .../home/menu/sidebar/sidebar_top_menu.dart | 23 +++++++++------- .../presentation/home/navigation.dart | 27 ++++++++++--------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/frontend/appflowy_flutter/lib/core/frameless_window.dart b/frontend/appflowy_flutter/lib/core/frameless_window.dart index 82101de60782..d19cb17bc25f 100644 --- a/frontend/appflowy_flutter/lib/core/frameless_window.dart +++ b/frontend/appflowy_flutter/lib/core/frameless_window.dart @@ -1,7 +1,8 @@ -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; import 'dart:io' show Platform; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + class CocoaWindowChannel { CocoaWindowChannel._(); @@ -46,9 +47,7 @@ class MoveWindowDetectorState extends State { return GestureDetector( // https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack behavior: HitTestBehavior.translucent, - onDoubleTap: () async { - await CocoaWindowChannel.instance.zoom(); - }, + onDoubleTap: () async => CocoaWindowChannel.instance.zoom(), onPanStart: (DragStartDetails details) { winX = details.globalPosition.dx; winY = details.globalPosition.dy; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index e4d5f2fa3e69..ec72bf7f05f2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -1,5 +1,7 @@ import 'dart:io' show Platform; +import 'package:flutter/material.dart'; + import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -7,9 +9,8 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.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'; /// Sidebar top menu is the top bar of the sidebar. @@ -71,15 +72,19 @@ class SidebarTopMenu extends StatelessWidget { ); return FlowyTooltip( richMessage: textSpan, - child: FlowyIconButton( - width: 28, - hoverColor: Colors.transparent, - onPressed: () => context + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (_) => context .read() .add(const HomeSettingEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - icon: const FlowySvg( - FlowySvgs.hide_menu_m, + child: const FlowyHover( + style: HoverStyle.transparent(), + child: SizedBox( + width: 20, + child: FlowySvg( + FlowySvgs.hide_menu_m, + ), + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index 5d5b465437ee..bec29223304f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -8,7 +8,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -71,18 +71,19 @@ class FlowyNavigation extends StatelessWidget { context, LocaleKeys.sideBar_openSidebar.tr(), ), - child: FlowyIconButton( - width: 24, - hoverColor: Colors.transparent, - onPressed: () { - context - .read() - .add(const HomeSettingEvent.collapseMenu()); - }, - iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), - icon: FlowySvg( - FlowySvgs.hide_menu_m, - color: Theme.of(context).iconTheme.color, + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerDown: (_) => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + child: const FlowyHover( + style: HoverStyle.transparent(), + child: SizedBox( + width: 20, + child: FlowySvg( + FlowySvgs.hide_menu_m, + ), + ), ), ), ), From 4dad87cc9a979b3ef54449f49728d3b33b272309 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 2 Apr 2024 15:40:04 +0200 Subject: [PATCH 11/16] feat: theme dropdown + theme mode selection --- .../assets/images/appearance/dark.png | Bin 0 -> 3746 bytes .../assets/images/appearance/light.png | Bin 0 -> 3613 bytes .../assets/images/appearance/system.png | Bin 0 -> 4130 bytes .../menu/sidebar/folder/_folder_header.dart | 6 +- .../pages/settings_workspace_view.dart | 147 +++++++++++++++++- .../settings/shared/settings_dropdown.dart | 55 +++++++ frontend/appflowy_flutter/pubspec.yaml | 5 +- frontend/resources/translations/en.json | 9 +- 8 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/images/appearance/dark.png create mode 100644 frontend/appflowy_flutter/assets/images/appearance/light.png create mode 100644 frontend/appflowy_flutter/assets/images/appearance/system.png create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart diff --git a/frontend/appflowy_flutter/assets/images/appearance/dark.png b/frontend/appflowy_flutter/assets/images/appearance/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f40e6a884bc4062de740de27e6a56ffca4f171b9 GIT binary patch literal 3746 zcmV;T4qfqyP)R;&--Y5nR8UQzvObi z&ONX9cmMZy{{R0u=ORF8X=!Q5WHQguat-ZP!pEn@OH#^*uD|~JcERMP)9LqU*&GN2 zo~DDnUTUC+Azpal1$pP4cM95-?d|Qdp`k%^c6Lg~aj1SN`uh4L)v5LcAs&y*($Z2f zG&Cetfl-+C$jFGKx2I!5;+a?5cc=7Ij^a4Z(PZg6mLE&WN019b8!T7I};!-l85 z63dmhwzkT~#zwWx6%!$n$Q4w%Q&UsJYZ(j%`4598v#hL4GP#=Y;o)H!3WY=>kubz@ zS-t@#mq{!tDiYQ?hmH%2baRcqQ6}3yuZiS8Llecv=swsApdSL4aS64znN)}G*?v%( zP$t~HHGPa9a-zDr`Yk#arh9!(Y~Q|}OI3NXTumc`DrK(H;B$<^SxcuhnbvbYSB%mg#hOa%Rouq@WLRHu-|<{q<2vn|uM-du~+ zWZPw!*qpM`<;6S@`rYuEGiSaVVB$QID@hUrOTxi}2W2=MmfwH$@rL%}9nS`w;3B%= z3RTX!Moh5pbvw7t|E7$+pSzyy9CCiA&~IrZhX*m(-*5Jx{ne8QU=mnX%jBvjHGlqm z`PDn_YdG}Lv7MQ8ni|Dn_y9(R1~JgzmvwAHf^EI6t*uqBtCoZ#ZEZgrCD+G97?)s| zTxGHA*RNOWlP6Cqa#<#W0p}|T-*6{|EOgM-Th*dcL*}pxhEY$!VGn>k1!T*Tk8zMi zFk7u|Az5$POJJYTb-|Q-Mcx6sVG=$(!4Rbb2R=q#f}>q9CYOT+k_P1pRaI39cEQH0 zaACL@Pt;dqX<3Omws=+=kKdla(%ETqOw5)DYzjg&UEI$~X|UE?3LKR6vJ1wszYAxcNW_5>6Vw{mwATLK)n0Z3-KIQi5noAn;%i z2-EsHiR5DmWco9h${+(nUEP&nmzTH!8KI}^6v5;rI22`xB}#h!G-O$xUWjdcwsIs1s6n=UliZ4}#ur)fU_Aw&xJ1>Q;`yBMd zM&0<-i%G}@66rlDWcsGI^)ikgJ&Nw`F3{Tzirze5rwC>*fl1Xa7<&mU2@FbFu;*aN zte4K+z=tO&qI5&X3uU|<-I%$OAnnGD$z`J9!xI!y zc4EAuqC&b2n<1JAb$H>!OfKbceS$s0NQ7;r)QE8?4ANYw^R|4P$>rRv+we?vc}S)*YO$dR zW=UZGR(ItvxprTd?!IB`lb7894{B z$?N5RTo1u`Vg)2YcQ#7b(z7I@rYe$0u0FoBxgNXrwPPw{-I^7kTnwtKDzNpn7Iby@ zW;cX_`u4Kma z3PtX+Z@6ti#IXOK~Bw zYUOnp8A+17g>m+56l>QkFIcX-rw4!e+g9}T$MEal`~mOmc@Kkw@k#4fcTS`#pO-d` z*7EoRxZUZ(8tkF1+peUzC}_6l&2OgAyM$AqqdyRI5TL5LcZn~ppjYd$M<3!GDXyN| zgcF~1VCSwqxKN0MLiGAO*s=2++-ezi#9Vsd8-ejLg+v?p5YN9y|oeuH?k2DCRWQ;^2Yz^M2spM>|a;$4`6DyZbPe zuD1vo##&8GK zW$7=*h9c>qMow-;U1$68O6wt1mWN1k1kT0cWU-gF56mzYY|8+zwM%A?Xtc+96{?J_ zHhOLlKE|M4b?t`n!s$xgu-QXIg*95SkOliF4C6&;FM&r3+GA(6?rJM${dJG|@BrgQ zXdhQg0UzeTLJwB9$I<%99`Dc=%%QSs*qA)FQd@I1slpu(%cR4HC+M!;EHA;81|f1r ziCgxjY@29Z%1QeBf2S(J#*#^96x>>rKJ&6 zQ8tYeKs$E4!G8lTM(BBLoFQ|x9N5yJrzwfiIbhnl^9bggD(6h=>+4WgSBF)rR-vQg z6z0si3TxJ^R$=tIb!#v%(2wTk1w`~rR1(1#J4_>oG51UlmCBv{sQcnXAurb5qv;91zV?Vq4fT=g78A`!CmYGvIzIe#(2NQCVr*gmio za0rn-@h%IePMtQ}Or%CjORHKlk(VM&udvlg$fH?_yrHW!sg+mfnCf_dJYC4WYyDW5zR&OjyRaY3`&KX$2d$C98$y!b%Dm z3^g_9SKN0N#c#fh;@7vRZ5ta)PN7J6>rN7B?-NeO#b@ND#0%VaE{qqUrz*A|`g ztppV!kEg+MXb|a=u-bNxe}r`P<;WC=Mzla)K(g_Rq-_bhHiF>$J25hU8PZkL@c)MKgKbIB9)q6sX-9HaLzsu~pPs_t zt=}eMrxU{gjLci2jt38o4lJn@!`S?VgJ>$R!7rK;_}0Ekw4dlfdgd$y4(vv7|C>Ze znIdy!_Cg{khCs&w>NHG34))Q*3WFXuhHu)0qFvjNDX)TvoS3v>>r~5R9m)$^R zV+j5Acl6N8RhbLvW$Q-HgvK_xdfS!>jD5%=5B>N3U~DIqgwuI~YIMlzMhBss%9P^O z5B4K?d^e)02wqM866wp+MC>l4reBHV)z?CvK8ECMe)ZCDqMb<7!$?!(DTuVdsbk7g z!EHeeJwzTV%8hSj4V;e;P#twh*3U)o@P4x3`RZYbObX7ikB|w4aUoz^up!Tt2%Sw6 zmL}WV24XmWbWZZBX2@8t(m#HMHMV5d;zvW@Lu0ayj68+36U5NXo5n6&v^uK}8=1YY zLp01onj|B!{L9%txZ)G|W}6d+=;0E;z_dD}UqVNsDDkgzc)i zi5+QMux<$5NN=o??hee@eCfGqUSRUUuzD-B9N3?&a;;XmYJMc=9d;yVzNn<7!X;Ph zvedKk-L(%7uq`<2b5*c{4C^~#I?%}LGLru6+JZ0yTS*<2c-L;46>zBY`gm?dJlUnZ zyc|BVVH!C*a?>)QE!mO=BOiq->+3tendp}S6j64L!Ln4xJ+8@BWw>hzpNmguObzI4 zT2?0~vaMEV_YD5Hi1S8PpPX*wY)R0`9_^~}d@d=t{O^oomp6hP`OZrt8H6_kXjcxTx5- z;v7WCa&NnJBksNXwqDAmKd7|S{htV)qRp?t$EQUvs{$J?|2Gf+0Sn4?bS&^#RR910 M07*qoM6N<$f=NJGUH||9 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/assets/images/appearance/light.png b/frontend/appflowy_flutter/assets/images/appearance/light.png new file mode 100644 index 0000000000000000000000000000000000000000..49f32bf3aa4cc75c36a2edb0e533f232de712b9d GIT binary patch literal 3613 zcmV+&4&w2NP)5nG0NgInM{%X>i z#+WoES~RKJ8VmBF&??9R+bzoi%PtFu`whD@v%52WpEKv&Id{HRU6GZ&PjdI(d(OFE z&%E=T_q^}9vknB?w{KsNPN!dxWx4FuBgAyE&2`x-yXVfGb6M$mo;;K5@Xv({7yL3ugXN>6qmEr> zsuGEWs^<=`Gd@19w#&=Q{cTe>F)`t{GsjO(PP)P!xE>oDtL^Xazds(2KPsnO;k^C! z+kX>s-`TKX!?T!KaD-fJ-MZD?v}u#`!3Q5Wb#--o5QmEp#kH%+^9dRm8BtG7BIRM- z>gsAImslaxjm2V$SRoN4k=Nrn^fuWu?OEoOm6iGA8iGlvSx2K$f1l{>?frqAe?deF zHp-1Yh8Y5Fv5pX{)+CqfrluxW$WT1?_4PS(=FCwKJ2W&DAe!WHO=4?mY8jla>3EB58*KEi=(~A9a9^25+@amZ}XI`KMr`N4p z=ioD9Y%IMXjfp?W-K=mO>Nii7=IxOzIF3yo)V^6~kZI2{za;M%bFR#qhh&@NUPrfq zeLizf*>l=;=J?=!S@(#j2tly z@%MqZ@O1ZcI6T^pD;3l7A_~Dd+l#V4{{e`{;qBjx#IK%_A=Sr-9{mOV7`S~qFuvk1 z=$stEFHijrDJk1Nya#W!{1r1DU0o+p6|2Ib!-sIsJ@;~?SgQHwAQ6QiL&mhLFPzd& zq8nA@W-c31myJ46MXox;`P@I}OM)HS|Amjve}vA?W4L(nV#&5ESOl*D}ZrdSD-u`u@t81jR2IDvW2JX=NNHuLhI$Dow#k+fY{!`e0p%oh%?!}h@ z-~HYrNG6h~+E9h+nwpYr^JMs zvY9PV`#Ok?@``GN2toq#YHh-xib%IYu+d{C%#XcK}p{G`XE)w>{q<6(%@WMc zi-eXUmW0Z#TA1?oazaVT(TwGmSJt7Xehw;Q4I&Q>nYwtE$mDnu&p-bh4#`daaYyqEnvRw_uV; zh_*&ZBKN7Y1j!|-GQRfA!Z!Aa(w5KWc@ZJE7RCIy`uYa!+__WLzPo1^PM$o0%Lz%j zOP>-)O>Hd_46_ypL$Zx@?rOWSsY!({OJy7a8tiKD2s#JUT&|%fQP8S6cfc2Ae_lW~ zqgq~3Czd)S%SF*MhVmp8uE#NvvMT?=73CG!{K%t*KYtPe_T$~)z@Qia1isZ znTOUxtyp#Y?RjlTy6}R%d*8!d-?$quzq}O>Km46ir_B;92SyBQB$G2<7|dZK-Ju1D zCE4(2CuA5d6FG>Cek|l(2wX)(2C>?&2FmPEAHRB+8_Sm6s`R(Ly-hyw^+h|bjT;}r z>7H&JKhY@?@uN~5TyP|32()Lu7!p}!&Cl{jb6nZ;4LG5tu6Z%Wq(e)_wVPHRL#%qv zjQ1dj>gsB&T6M>i?dFP?F}H-LVUQ~ko{=s2p zgM}Fw*-O&b1l|utkr%IM0IesC7YK$5TYmo zS!L~(MJN@rR*^Dk5M>$n=^|^gSs&3Vkr#40e!LT}y|xWQ!=EnB%NXmGhCnB0G}!VG z9PO6*62XPK4D0={+zzhy^l1$A4@jK%b$obWKWS`Qw?H}Ah69UUENZWjaX z*UX(Oan*jg_5{8*e*u;*ZNZge4EWznev*Izbh!^{wDh%*+< zPL2zMKgOFdI#J4$yr{4J%aOt zJ0#Y~hmnd>C$9o=TADDF< z<=N>YLpIC8QQXw9{a|ISwiRnxP^9WN7k$qnQ)j;+nO2$E^ekeXNy#$FCuaPCAoL{4 zj2}!3){Dw0i`7Onsmec=#%4|g{bS`9b8T~(5Ko~HxxKx;`1t(!ymBX%#Frf@d0sOY ztr(2^MZzN5Q}C?8g{R+SHy?nSH(}~A1>V`Q9lLk$#I9ZM;P9bC*$Q#Zs!|<7aw4-U zH@a`%UNulLID6)d%Jla1^vH*Fy683dfNX1nFQ|-+j0DQsjvU4%LGf>FR)e)wI@&#R zwR`3mT{d%ujWlp67I1&*r5EwQ0}rYsBIin&)b{)T`FEzlXcuZKPb1oN9h~9)xG++S z5051flkD=I-Mg_*(&7UH17-ppYU}D0S^PRKx#=dHJaIy0fS-8cC;0s{zr_uU7Nfhn z8&$Cwx=wYWrR8Q--zffer}*RZ7hH$<*qEwYDs^8Ja)-*wI!UyV0a~AJRUe zEVzIlv1!w{6}bc_YZ&<3vH3{$9Yyl^QQh#K9s zv?!t{#>a8ry7d?p%N!aS6ssN2oT{pt`9w4|HYrj$zpTEl9;;WcMq@*xw9CBWz`AJU ziyh1uT{iQBUE_{f0}Iuu(RwYf!nSIsP%upbM*jBAH{T*d<^$^Oo13hm@Wru4IL*IC z>Z9F=id-}|H)HeW@B8)Uw;^)D0SjjhnQ4blKKavZyPQGG_17=LU3af8?t@=q1pHuH zrmO;9gCUglTASVc(Op)8qGD-@R4SlJS6@9xExCwX`z^Kh=09BU=1P&WX3bjvixX&9 z;FwEIF3&HWRMBewF$AmEFL{7%!8WPZa2YoF28=UQa0=8ca??12w8V!P7-+b*1eh{Q z9(=1conB@;c*a8h;A}-eBD9;PGRkHor%66twKXbxJN5n@W%Zkn&b#zK)L($OD) z|C*P!=vg&#`;-W3u-0GSTd;{m)(7^>LUKzZS0Y?pVAcU*wH_NQHZkE)B1*u?c@fdm zFSVGjzhij{KCxP({p?1VwJ(mmPR``uV99~$^o;L|TrCdCEro)y2-CS4@4?mb0Yf-4 zGLj)UwBRXWh9Pv*jHTN8>l(S>AN~7{id^M!@MjD|hKnI!!TtmjtHU#v?AnP=6G_Wh zb4YG6|bcmVYdonSA z5C!4?UWAbM;qdVAN|VgANo0`6%4(=`v#{;iv#0l___lY5JP4;G`Bf79n55emOWtd_ zq=RBwJ3RfehEJ|0+O^kmT>Mn`iGL;%@$(3wz$EMm$s2ZbbPS5y`!gjJ`acnDkwHzb=6h(ocC{m|D z+@gik8jTynZQ!O+n$$sH<>e?55J?U#%Z@}-wk(SxB~dRvB)%WVdvCsPZf7}nJX&(7 zla>L0z~1c6&hE_o_PcLpzHeqU5VmaDvN9fzzf7ksbcC>o`C_lz?f&Z{M~?Jp7P;?e zn)ZAmk@)wHj*ii5J#Ct%MuB$l;6Z)s)~$T5_x1G|x~^-6Vd#8}jg4t8mrI<7!(qL! zuuz+xo)*`XmquyzXf(=!xVEBnDvK`9IS&Sv%dkQ_feA(|787w63?`B9S!qNCtU?>sN8m~tRA^BdCWt>4mq(Jy@hY)=SOm&@h9sNw zl;m>WoQFcDO|F`U^|3f_!+N(N^O0o;1On#9jT=<)71JvT+2``v8lX#_A~Nb>5w1Ml#*oTW2GT{D8Z84>1gJa>`yxT88ai~M`n_kbG}c<9FY1p7K?m?$%R7^ELyT# zlIuj|Bv|Pw=w6?9CmalyVcY=+W0s0|=9yT zJ23tR_OE{hHyWb$JThZ)dwP1r;DLvuJU|#9AI~Xv6G@g(%{ov2KjTT|{jf7_5d?bO# zPWNNSk^p{iWm3eopVRT=OJ01cDT)8s=N571sT7hg#i7+{Nc|#-{9v{cHp?rKc>FJ+ zpL`F=&0Amw3n{h`@sIrz^s%Ezm2QXW4q*iu4xg?4JG4)}i|r-%ARpky%-p+oZ-DhJ z6G~PvK`a?-w8_ayea8b&3{Fjj;Z&SB)&{5Iu20;amSCxGrPT64GqAg;cuvG-Rc7nm6?IfdUjl2-f*TD`K0U^Osa>=!0 zyKR`s9%KxhuX0Br~8mk#KLLG_4K9Bo$ z{4qK^PeC_asH&>M9e3P`8x0|$EVX=)#2RGq5k7F1-fRg@re+l0bT~{p&QzR%nJ*@# zgWH*DU=H-3Yjo^!|tr#wjyD9aiarXLxA5mAiSF}Pkw~O)D_|-gkBeJewi?sa zpv59qH_aPBAh`6F^dK05V0A{Lb*?E>kTSp(I)8aY9F9PiHI2KHfud>%_3Hpg2{vZt z11|c5iH4-GZ^JO+w4Ye5g#44ir`Sr zAXle{ViEI#lwd(7&(W0=nPE+O#vwWZw&MguzL(8OP0_-t`(i9Pr>bWk+m-=yfFEInEdz3E3a(Vl!bVhS%aqk>0`V1 zI(hQn`|WLWcGKjttL*cvM0s;f+mruP$%VE26n_9CIhD{k+v_~H<8~v*lm2+sq$GN= z7ja`?H*!#xoLb9Eb!%Zv1`=h}NR*VpDOeEJG}$dO1i=$=z7Qf$!Hpg6FOMAXl^&=SAdfC{X*>qcdv7A;D-dFsT+s+E z8b+dKHC#QNNR^br43)z;`vLTc5hTm1c-IT0C?7pC4ee?llC_Ogg5ZZ%@X)~n7`QS3 zH+hd68`q$zxjB7_2vb-AJ-~0@* z&7X#+qYd%qJK@{^{WSSwgCZ|?m_}%kFwDXbjG;c@3#(>T_o;hT@jHFs%a;+^^q(o2`mll}$?w%m)j(`FgoZvO?kicMS&7vhq3FZB9mxH~%#Z`gojX%)<1 z5x1^X0_xx=<4COh7)j(Bo)!dG_i^&U`(XNmNYt)HvVJ|!j)Bp44vC88FpJ5f&8ywT z5-rTK_dNLE!!!^|Pxj-PM-ujVpcb*g12BjG3#nuc%!;pJ`SL;RqtV~k_$bQDDyUd4 z)YPn`k!vCP`}>g~_4m2Y@1T+3X^f4H+Nvl}5M-w@N|%&kaPX>_#tpdWk`B)K0q(A>c#-hq8RR}xhJLMLh|5XuPul<~+1?`58^=y>?6-q-_S-Dgk*>J7+ zlXRJDADZL6a}NQwZoSvmof+wJJR_Gk3In^c^@`BP*&{jEP(P|)_zE0_oipp?*v{bN zDNqjTrr9b&C0Fjov=9vL*<3npSxJV=C>P0C#Jr#mH0+ z7J{+INyFk?$RJqSBA{#)h(*i`QblBJ%*Mo8Yv&fmT4e{r9?4}lbUfP!AF}P2W8iRe zWrl-Ost4;>l;9fxTY}}bua>dhmS{K}nlOwyd}fcdMOeLtj6HGTcr1WkgxWPzh+Y^f z!r{p1rUA%k?63eDIj8Rz!32&D4Gjep=~cJc*1TTtHMbnUrEn<2lamu7WP&^P?Add7 zwnrJ(=ELjPHol8Rd}O@#+Rr)oG4J7isIRZ@Ys16CT5WCZkk98!sXIB}dHcr8-Di4y zy?q1MwwB)lJpI%c;Ur9wYiZA(|HX?h{^uxJBX@~jWJ^oSN*X5r8i_@%07*qoM6N<$g0<24x&QzG literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart index 00f88e153dbc..a33d3d33743c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -1,6 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class FolderHeader extends StatefulWidget { const FolderHeader({ @@ -42,6 +44,8 @@ class _FolderHeaderState extends State { ), padding: const EdgeInsets.all(textPadding), fillColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + fontHoverColor: Theme.of(context).colorScheme.onPrimary, onPressed: widget.onPressed, ), if (onHover) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index e35c3f703ae3..1c07d3a677e3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,17 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsWorkspaceView extends StatefulWidget { @@ -129,14 +139,18 @@ class _SettingsWorkspaceViewState extends State { const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_appearance_title.tr(), - children: const [], + children: const [ + _AppearanceSelector(), + ], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_theme_title.tr(), description: LocaleKeys.settings_workspace_theme_description.tr(), - children: const [], + children: const [ + _ThemeDropdown(), + ], ), const SettingsCategorySpacer(), SettingsCategory( @@ -165,3 +179,132 @@ class _SettingsWorkspaceViewState extends State { ); } } + +class _ThemeDropdown extends StatelessWidget { + const _ThemeDropdown({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DynamicPluginBloc()..add(DynamicPluginEvent.load()), + child: BlocBuilder( + buildWhen: (previous, current) => current is Ready, + builder: (context, state) { + final currentTheme = + context.read().state.appTheme.themeName; + + final customThemes = state.maybeWhen( + ready: (plugins) => + plugins.map((p) => p.theme).whereType(), + orElse: () => null, + ); + + return SettingsDropdown( + onChanged: (appTheme) => + context.read().setTheme(appTheme), + selectedOption: currentTheme, + options: [ + ...AppTheme.builtins.map( + (e) => DropdownMenuEntry( + value: e.themeName, + label: e.themeName, + ), + ), + ...?customThemes?.map( + (e) => DropdownMenuEntry( + value: e.themeName, + label: e.themeName, + trailingIcon: FlowyIconButton( + onPressed: () { + context.read().add( + DynamicPluginEvent.removePlugin( + name: e.themeName, + ), + ); + + if (currentTheme == e.themeName) { + context.read().setTheme( + AppTheme.builtins.first.themeName, + ); + } + }, + icon: const FlowySvg(FlowySvgs.delete_s), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _AppearanceSelector extends StatelessWidget { + const _AppearanceSelector(); + + @override + Widget build(BuildContext context) { + final themeMode = context.read().state.themeMode; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...ThemeMode.values.map( + (t) => Padding( + padding: const EdgeInsets.only(right: 16), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => + context.read().setThemeMode(t), + child: FlowyHover( + style: HoverStyle.transparent( + foregroundColorOnHover: + AFThemeExtension.of(context).textColor, + ), + child: Column( + children: [ + Container( + width: 88, + height: 72, + decoration: BoxDecoration( + border: Border.all( + color: t == themeMode + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.outline, + ), + borderRadius: Corners.s4Border, + image: DecorationImage( + image: AssetImage( + 'assets/images/appearance/${t.name.toLowerCase()}.png', + ), + fit: BoxFit.cover, + ), + ), + ), + const VSpace(6), + FlowyText.regular( + getLabel(t), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ), + ], + ); + } + + String getLabel(ThemeMode t) => switch (t) { + ThemeMode.system => + LocaleKeys.settings_workspace_appearance_options_system.tr(), + ThemeMode.light => + LocaleKeys.settings_workspace_appearance_options_light.tr(), + ThemeMode.dark => + LocaleKeys.settings_workspace_appearance_options_dark.tr(), + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart new file mode 100644 index 000000000000..84b49de8def8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class SettingsDropdown extends StatefulWidget { + const SettingsDropdown({ + super.key, + required this.selectedOption, + required this.options, + this.onChanged, + this.actions, + }); + + final String selectedOption; + final List> options; + final void Function(String)? onChanged; + final List? actions; + + @override + State createState() => _SettingsDropdownState(); +} + +class _SettingsDropdownState extends State { + late final TextEditingController controller; + + @override + void initState() { + super.initState(); + controller = TextEditingController(text: widget.selectedOption); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + DropdownMenu( + controller: controller, + menuStyle: + MenuStyle(visualDensity: VisualDensity.adaptivePlatformDensity), + initialSelection: widget.selectedOption, + dropdownMenuEntries: widget.options, + onSelected: (value) => + value != null ? widget.onChanged?.call(value) : null, + ), + if (widget.actions?.isNotEmpty == true) ...[ + const HSpace(16), + SeparatedRow( + separatorBuilder: () => const HSpace(16), + children: widget.actions!, + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 3bc2a9ba3c1c..3b854b22dab9 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -237,13 +237,14 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/images/appearance/ + - assets/images/emoji/ + - assets/images/login/ - assets/flowy_icons/ - assets/flowy_icons/16x/ - assets/flowy_icons/24x/ - assets/flowy_icons/32x/ - assets/flowy_icons/40x/ - - assets/images/emoji/ - - assets/images/login/ - assets/translations/ # The following assets will be excluded in release. diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7c2730927bc6..444e28c9fbea 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -339,7 +339,12 @@ }, "appearance": { "title": "Appearance", - "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language." + "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language.", + "options": { + "system": "Auto", + "light": "Light", + "dark": "Dark" + } }, "theme": { "title": "Theme", @@ -1499,4 +1504,4 @@ "noNetworkConnected": "No network connected" } } -} \ No newline at end of file +} From 10619e1d0ccc4460f403ccbd29acd093eac6df5a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 9 Apr 2024 22:10:12 +0200 Subject: [PATCH 12/16] feat: more of workspace --- .../lib/flutter/af_dropdown_menu.dart | 1044 +++++++++++++++++ .../workspace/workspace_settings_bloc.dart | 8 +- .../workspace/_sidebar_workspace_icon.dart | 2 +- .../pages/settings_workspace_view.dart | 271 ++++- .../shared/af_dropdown_menu_entry.dart | 53 + .../shared/settings_actionable_input.dart | 4 + .../settings/shared/settings_dropdown.dart | 67 +- .../shared/settings_radio_select.dart | 74 ++ .../settings_appearance/color_scheme.dart | 19 +- .../flowy_icons/24x/textdirection_auto.svg | 8 + .../flowy_icons/24x/textdirection_ltr.svg | 8 + .../flowy_icons/24x/textdirection_rtl.svg | 8 + frontend/resources/translations/en.json | 8 +- 13 files changed, 1516 insertions(+), 58 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart create mode 100644 frontend/resources/flowy_icons/24x/textdirection_auto.svg create mode 100644 frontend/resources/flowy_icons/24x/textdirection_ltr.svg create mode 100644 frontend/resources/flowy_icons/24x/textdirection_rtl.svg diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart new file mode 100644 index 000000000000..bd08d987994f --- /dev/null +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -0,0 +1,1044 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(Mathias): Make a PR in Flutter repository that enables customizing +// the dropdown menu without having to copy the entire file. +// This is a temporary solution! + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +const double _kMinimumWidth = 112.0; + +const double _kDefaultHorizontalPadding = 12.0; + +// Navigation shortcuts to move the selected menu items up or down. +final Map _kMenuTraversalShortcuts = + { + LogicalKeySet(LogicalKeyboardKey.arrowUp): const _ArrowUpIntent(), + LogicalKeySet(LogicalKeyboardKey.arrowDown): const _ArrowDownIntent(), +}; + +/// A dropdown menu that can be opened from a [TextField]. The selected +/// menu item is displayed in that field. +/// +/// This widget is used to help people make a choice from a menu and put the +/// selected item into the text input field. People can also filter the list based +/// on the text input or search one item in the menu list. +/// +/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information, +/// such as: label, leading icon or trailing icon for each entry. The [TextField] +/// will be updated based on the selection from the menu entries. The text field +/// will stay empty if the selected entry is disabled. +/// +/// The dropdown menu can be traversed by pressing the up or down key. During the +/// process, the corresponding item will be highlighted and the text field will be updated. +/// Disabled items will be skipped during traversal. +/// +/// The menu can be scrollable if not all items in the list are displayed at once. +/// +/// {@tool dartpad} +/// This sample shows how to display outlined [AFDropdownMenu] and filled [AFDropdownMenu]. +/// +/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus. +/// The [AFDropdownMenu] uses a [TextField] as the "anchor". +/// * [TextField], which is a text input widget that uses an [InputDecoration]. +/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [AFDropdownMenu] list. +class AFDropdownMenu extends StatefulWidget { + /// Creates a const [AFDropdownMenu]. + /// + /// The leading and trailing icons in the text field can be customized by using + /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are + /// passed down to the [InputDecoration] properties, and will override values + /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon]. + /// + /// Except leading and trailing icons, the text field can be configured by the + /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle]. + const AFDropdownMenu({ + super.key, + this.enabled = true, + this.width, + this.menuHeight, + this.leadingIcon, + this.trailingIcon, + this.label, + this.hintText, + this.helperText, + this.errorText, + this.selectedTrailingIcon, + this.enableFilter = false, + this.enableSearch = true, + this.textStyle, + this.inputDecorationTheme, + this.menuStyle, + this.controller, + this.initialSelection, + this.onSelected, + this.requestFocusOnTap, + this.expandedInsets, + this.searchCallback, + required this.dropdownMenuEntries, + this.menuDecoration, + }); + + /// Determine if the [AFDropdownMenu] is enabled. + /// + /// Defaults to true. + final bool enabled; + + /// Determine the width of the [AFDropdownMenu]. + /// + /// If this is null, the width of the [AFDropdownMenu] will be the same as the width of the widest + /// menu item plus the width of the leading/trailing icon. + final double? width; + + /// Determine the height of the menu. + /// + /// If this is null, the menu will display as many items as possible on the screen. + final double? menuHeight; + + /// An optional Icon at the front of the text input field. + /// + /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned + /// with the text in the text field. + final Widget? leadingIcon; + + /// An optional icon at the end of the text field. + /// + /// Defaults to an [Icon] with [Icons.arrow_drop_down]. + final Widget? trailingIcon; + + /// Optional widget that describes the input field. + /// + /// When the input field is empty and unfocused, the label is displayed on + /// top of the input field (i.e., at the same location on the screen where + /// text may be entered in the input field). When the input field receives + /// focus (or if the field is non-empty), the label moves above, either + /// vertically adjacent to, or to the center of the input field. + /// + /// Defaults to null. + final Widget? label; + + /// Text that suggests what sort of input the field accepts. + /// + /// Defaults to null; + final String? hintText; + + /// Text that provides context about the [AFDropdownMenu]'s value, such + /// as how the value will be used. + /// + /// If non-null, the text is displayed below the input field, in + /// the same location as [errorText]. If a non-null [errorText] value is + /// specified then the helper text is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. + final String? helperText; + + /// Text that appears below the input field and the border to show the error message. + /// + /// If non-null, the border's color animates to red and the [helperText] is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. + final String? errorText; + + /// An optional icon at the end of the text field to indicate that the text + /// field is pressed. + /// + /// Defaults to an [Icon] with [Icons.arrow_drop_up]. + final Widget? selectedTrailingIcon; + + /// Determine if the menu list can be filtered by the text input. + /// + /// Defaults to false. + final bool enableFilter; + + /// Determine if the first item that matches the text input can be highlighted. + /// + /// Defaults to true as the search function could be commonly used. + final bool enableSearch; + + /// The text style for the [TextField] of the [AFDropdownMenu]; + /// + /// Defaults to the overall theme's [TextTheme.bodyLarge] + /// if the dropdown menu theme's value is null. + final TextStyle? textStyle; + + /// Defines the default appearance of [InputDecoration] to show around the text field. + /// + /// By default, shows a outlined text field. + final InputDecorationTheme? inputDecorationTheme; + + /// The [MenuStyle] that defines the visual attributes of the menu. + /// + /// The default width of the menu is set to the width of the text field. + final MenuStyle? menuStyle; + + /// Controls the text being edited or selected in the menu. + /// + /// If null, this widget will create its own [TextEditingController]. + final TextEditingController? controller; + + /// The value used to for an initial selection. + /// + /// Defaults to null. + final T? initialSelection; + + /// The callback is called when a selection is made. + /// + /// Defaults to null. If null, only the text field is updated. + final ValueChanged? onSelected; + + /// Determine if the dropdown button requests focus and the on-screen virtual + /// keyboard is shown in response to a touch event. + /// + /// By default, on mobile platforms, tapping on the text field and opening + /// the menu will not cause a focus request and the virtual keyboard will not + /// appear. The default behavior for desktop platforms is for the dropdown to + /// take the focus. + /// + /// Defaults to null. Setting this field to true or false, rather than allowing + /// the implementation to choose based on the platform, can be useful for + /// applications that want to override the default behavior. + final bool? requestFocusOnTap; + + /// Descriptions of the menu items in the [AFDropdownMenu]. + /// + /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] + /// is provided. If this is an empty list, the menu will be empty and only + /// contain space for padding. + final List> dropdownMenuEntries; + + /// Defines the menu text field's width to be equal to its parent's width + /// plus the horizontal width of the specified insets. + /// + /// If this property is null, the width of the text field will be determined + /// by the width of menu items or [AFDropdownMenu.width]. If this property is not null, + /// the text field's width will match the parent's width plus the specified insets. + /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same + /// as its parent's width. + /// + /// The [expandedInsets]' top and bottom are ignored, only its left and right + /// properties are used. + /// + /// Defaults to null. + final EdgeInsets? expandedInsets; + + /// When [AFDropdownMenu.enableSearch] is true, this callback is used to compute + /// the index of the search result to be highlighted. + /// + /// {@tool snippet} + /// + /// In this example the `searchCallback` returns the index of the search result + /// that exactly matches the query. + /// + /// ```dart + /// DropdownMenu( + /// searchCallback: (List> entries, String query) { + /// if (query.isEmpty) { + /// return null; + /// } + /// final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label == query); + /// + /// return index != -1 ? index : null; + /// }, + /// dropdownMenuEntries: const >[], + /// ) + /// ``` + /// {@end-tool} + /// + /// Defaults to null. If this is null and [AFDropdownMenu.enableSearch] is true, + /// the default function will return the index of the first matching result + /// which contains the contents of the text input field. + final SearchCallback? searchCallback; + + final BoxDecoration? menuDecoration; + + @override + State> createState() => _AFDropdownMenuState(); +} + +class _AFDropdownMenuState extends State> { + final GlobalKey _anchorKey = GlobalKey(); + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); + late bool _enableFilter; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; + double? leadingPadding; + bool _menuHasEnabledItem = false; + TextEditingController? _localTextEditingController; + TextEditingController get _textEditingController { + return widget.controller ?? + (_localTextEditingController ??= TextEditingController()); + } + + @override + void initState() { + super.initState(); + _enableFilter = widget.enableFilter; + filteredEntries = widget.dropdownMenuEntries; + buttonItemKeys = List.generate( + filteredEntries.length, + (int index) => GlobalKey(), + ); + _menuHasEnabledItem = + filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + + final int index = filteredEntries.indexWhere( + (DropdownMenuEntry entry) => entry.value == widget.initialSelection, + ); + if (index != -1) { + _textEditingController.value = TextEditingValue( + text: filteredEntries[index].label, + selection: TextSelection.collapsed( + offset: filteredEntries[index].label.length, + ), + ); + } + refreshLeadingPadding(); + } + + @override + void dispose() { + _localTextEditingController?.dispose(); + _localTextEditingController = null; + super.dispose(); + } + + @override + void didUpdateWidget(AFDropdownMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + if (widget.controller != null) { + _localTextEditingController?.dispose(); + _localTextEditingController = null; + } + } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + currentHighlight = null; + } + } + if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { + currentHighlight = null; + filteredEntries = widget.dropdownMenuEntries; + buttonItemKeys = List.generate( + filteredEntries.length, + (int index) => GlobalKey(), + ); + _menuHasEnabledItem = + filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + } + if (oldWidget.leadingIcon != widget.leadingIcon) { + refreshLeadingPadding(); + } + if (oldWidget.initialSelection != widget.initialSelection) { + final int index = filteredEntries.indexWhere( + (DropdownMenuEntry entry) => entry.value == widget.initialSelection, + ); + if (index != -1) { + _textEditingController.value = TextEditingValue( + text: filteredEntries[index].label, + selection: TextSelection.collapsed( + offset: filteredEntries[index].label.length, + ), + ); + } + } + } + + bool canRequestFocus() { + if (widget.requestFocusOnTap != null) { + return widget.requestFocusOnTap!; + } + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return false; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + return true; + } + } + + void refreshLeadingPadding() { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + setState(() { + leadingPadding = getWidth(_leadingKey); + }); + }, + debugLabel: 'DropdownMenu.refreshLeadingPadding', + ); + } + + void scrollToHighlight() { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final BuildContext? highlightContext = + buttonItemKeys[currentHighlight!].currentContext; + if (highlightContext != null) { + Scrollable.ensureVisible(highlightContext); + } + }, + debugLabel: 'DropdownMenu.scrollToHighlight', + ); + } + + double? getWidth(GlobalKey key) { + final BuildContext? context = key.currentContext; + if (context != null) { + final RenderBox box = context.findRenderObject()! as RenderBox; + return box.hasSize ? box.size.width : null; + } + return null; + } + + List> filter( + List> entries, + TextEditingController textEditingController, + ) { + final String filterText = textEditingController.text.toLowerCase(); + return entries + .where( + (DropdownMenuEntry entry) => + entry.label.toLowerCase().contains(filterText), + ) + .toList(); + } + + int? search( + List> entries, + TextEditingController textEditingController, + ) { + final String searchText = textEditingController.value.text.toLowerCase(); + if (searchText.isEmpty) { + return null; + } + final int index = entries.indexWhere( + (DropdownMenuEntry entry) => + entry.label.toLowerCase().contains(searchText), + ); + + return index != -1 ? index : null; + } + + List _buildButtons( + List> filteredEntries, + TextDirection textDirection, { + int? focusedIndex, + bool enableScrollToHighlight = true, + }) { + final List result = []; + for (int i = 0; i < filteredEntries.length; i++) { + final DropdownMenuEntry entry = filteredEntries[i]; + + // By default, when the text field has a leading icon but a menu entry doesn't + // have one, the label of the entry should have extra padding to be aligned + // with the text in the text input field. When both the text field and the + // menu entry have leading icons, the menu entry should remove the extra + // paddings so its leading icon will be aligned with the leading icon of + // the text field. + final double padding = entry.leadingIcon == null + ? (leadingPadding ?? _kDefaultHorizontalPadding) + : _kDefaultHorizontalPadding; + final ButtonStyle defaultStyle; + switch (textDirection) { + case TextDirection.rtl: + defaultStyle = MenuItemButton.styleFrom( + padding: EdgeInsets.only( + left: _kDefaultHorizontalPadding, + right: padding, + ), + ); + case TextDirection.ltr: + defaultStyle = MenuItemButton.styleFrom( + padding: EdgeInsets.only( + left: padding, + right: _kDefaultHorizontalPadding, + ), + ); + } + + ButtonStyle effectiveStyle = entry.style ?? defaultStyle; + final Color focusedBackgroundColor = effectiveStyle.foregroundColor + ?.resolve({MaterialState.focused}) ?? + Theme.of(context).colorScheme.onSurface; + + Widget label = entry.labelWidget ?? Text(entry.label); + if (widget.width != null) { + final double horizontalPadding = padding + _kDefaultHorizontalPadding; + label = ConstrainedBox( + constraints: + BoxConstraints(maxWidth: widget.width! - horizontalPadding), + child: label, + ); + } + + // Simulate the focused state because the text field should always be focused + // during traversal. If the menu item has a custom foreground color, the "focused" + // color will also change to foregroundColor.withOpacity(0.12). + effectiveStyle = entry.enabled && i == focusedIndex + ? effectiveStyle.copyWith( + backgroundColor: + MaterialStatePropertyAll(focusedBackgroundColor), + ) + : effectiveStyle; + + final Widget menuItemButton = Padding( + padding: const EdgeInsets.only(bottom: 6), + child: MenuItemButton( + key: enableScrollToHighlight ? buttonItemKeys[i] : null, + style: effectiveStyle, + leadingIcon: entry.leadingIcon, + trailingIcon: entry.trailingIcon, + onPressed: entry.enabled + ? () { + _textEditingController.value = TextEditingValue( + text: entry.label, + selection: + TextSelection.collapsed(offset: entry.label.length), + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); + } + : null, + requestFocusOnHover: false, + child: label, + ), + ); + result.add(menuItemButton); + } + + return result; + } + + void handleUpKeyInvoke(_) { + setState(() { + if (!_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + currentHighlight ??= 0; + currentHighlight = (currentHighlight! - 1) % filteredEntries.length; + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! - 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _textEditingController.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } + + void handleDownKeyInvoke(_) { + setState(() { + if (!_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + currentHighlight ??= -1; + currentHighlight = (currentHighlight! + 1) % filteredEntries.length; + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! + 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _textEditingController.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } + + void handlePressed(MenuController controller) { + if (controller.isOpen) { + currentHighlight = null; + controller.close(); + } else { + // close to open + if (_textEditingController.text.isNotEmpty) { + _enableFilter = false; + } + controller.open(); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final TextDirection textDirection = Directionality.of(context); + _initialMenu ??= _buildButtons( + widget.dropdownMenuEntries, + textDirection, + enableScrollToHighlight: false, + ); + final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); + final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); + + if (_enableFilter) { + filteredEntries = + filter(widget.dropdownMenuEntries, _textEditingController); + } + + if (widget.enableSearch) { + if (widget.searchCallback != null) { + currentHighlight = widget.searchCallback! + .call(filteredEntries, _textEditingController.text); + } else { + currentHighlight = search(filteredEntries, _textEditingController); + } + if (currentHighlight != null) { + scrollToHighlight(); + } + } + + final List menu = _buildButtons( + filteredEntries, + textDirection, + focusedIndex: currentHighlight, + ); + + final TextStyle? effectiveTextStyle = + widget.textStyle ?? theme.textStyle ?? defaults.textStyle; + + MenuStyle? effectiveMenuStyle = + widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!; + + final double? anchorWidth = getWidth(_anchorKey); + if (widget.width != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + minimumSize: MaterialStatePropertyAll(Size(widget.width!, 0.0)), + ); + } else if (anchorWidth != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + minimumSize: MaterialStatePropertyAll(Size(anchorWidth, 0.0)), + ); + } + + if (widget.menuHeight != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + maximumSize: MaterialStatePropertyAll( + Size(double.infinity, widget.menuHeight!), + ), + ); + } + final InputDecorationTheme effectiveInputDecorationTheme = + widget.inputDecorationTheme ?? + theme.inputDecorationTheme ?? + defaults.inputDecorationTheme!; + + final MouseCursor effectiveMouseCursor = + canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click; + + Widget menuAnchor = MenuAnchor( + style: effectiveMenuStyle, + controller: _controller, + menuChildren: menu, + crossAxisUnconstrained: false, + builder: ( + BuildContext context, + MenuController controller, + Widget? child, + ) { + assert(_initialMenu != null); + final Widget trailingButton = Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + splashRadius: 1, + isSelected: controller.isOpen, + icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), + selectedIcon: + widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), + onPressed: () { + handlePressed(controller); + }, + ), + ); + + final Widget leadingButton = Padding( + padding: const EdgeInsets.all(8.0), + child: widget.leadingIcon ?? const SizedBox(), + ); + + final Widget textField = TextField( + key: _anchorKey, + mouseCursor: effectiveMouseCursor, + canRequestFocus: canRequestFocus(), + enableInteractiveSelection: canRequestFocus(), + textAlignVertical: TextAlignVertical.center, + style: effectiveTextStyle, + controller: _textEditingController, + onEditingComplete: () { + if (currentHighlight != null) { + final DropdownMenuEntry entry = + filteredEntries[currentHighlight!]; + if (entry.enabled) { + _textEditingController.value = TextEditingValue( + text: entry.label, + selection: + TextSelection.collapsed(offset: entry.label.length), + ); + widget.onSelected?.call(entry.value); + } + } else { + widget.onSelected?.call(null); + } + if (!widget.enableSearch) { + currentHighlight = null; + } + controller.close(); + }, + onTap: () { + handlePressed(controller); + }, + onChanged: (String text) { + controller.open(); + setState(() { + filteredEntries = widget.dropdownMenuEntries; + _enableFilter = widget.enableFilter; + }); + }, + decoration: InputDecoration( + enabled: widget.enabled, + label: widget.label, + hintText: widget.hintText, + helperText: widget.helperText, + errorText: widget.errorText, + prefixIcon: widget.leadingIcon != null + ? Container(key: _leadingKey, child: widget.leadingIcon) + : null, + suffixIcon: trailingButton, + ).applyDefaults(effectiveInputDecorationTheme), + ); + + if (widget.expandedInsets != null) { + // If [expandedInsets] is not null, the width of the text field should depend + // on its parent width. So we don't need to use `_DropdownMenuBody` to + // calculate the children's width. + return textField; + } + + return _DropdownMenuBody( + width: widget.width, + children: [ + textField, + for (final Widget item in _initialMenu!) item, + trailingButton, + leadingButton, + ], + ); + }, + ); + + if (widget.expandedInsets != null) { + menuAnchor = Container( + alignment: AlignmentDirectional.topStart, + padding: widget.expandedInsets?.copyWith(top: 0.0, bottom: 0.0), + child: menuAnchor, + ); + } + + return Shortcuts( + shortcuts: _kMenuTraversalShortcuts, + child: Actions( + actions: >{ + _ArrowUpIntent: CallbackAction<_ArrowUpIntent>( + onInvoke: handleUpKeyInvoke, + ), + _ArrowDownIntent: CallbackAction<_ArrowDownIntent>( + onInvoke: handleDownKeyInvoke, + ), + }, + child: menuAnchor, + ), + ); + } +} + +class _ArrowUpIntent extends Intent { + const _ArrowUpIntent(); +} + +class _ArrowDownIntent extends Intent { + const _ArrowDownIntent(); +} + +class _DropdownMenuBody extends MultiChildRenderObjectWidget { + const _DropdownMenuBody({ + super.children, + this.width, + }); + + final double? width; + + @override + _RenderDropdownMenuBody createRenderObject(BuildContext context) { + return _RenderDropdownMenuBody( + width: width, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderDropdownMenuBody renderObject, + ) { + renderObject.width = width; + } +} + +class _DropdownMenuBodyParentData extends ContainerBoxParentData {} + +class _RenderDropdownMenuBody extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + _RenderDropdownMenuBody({ + double? width, + }) : _width = width; + + double? get width => _width; + double? _width; + set width(double? value) { + if (_width == value) { + return; + } + _width = value; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _DropdownMenuBodyParentData) { + child.parentData = _DropdownMenuBodyParentData(); + } + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + double maxWidth = 0.0; + double? maxHeight; + RenderBox? child = firstChild; + + final BoxConstraints innerConstraints = BoxConstraints( + maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), + maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), + ); + while (child != null) { + if (child == firstChild) { + child.layout(innerConstraints, parentUsesSize: true); + maxHeight ??= child.size.height; + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + continue; + } + child.layout(innerConstraints, parentUsesSize: true); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + childParentData.offset = Offset.zero; + maxWidth = math.max(maxWidth, child.size.width); + maxHeight ??= child.size.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + + assert(maxHeight != null); + maxWidth = math.max(_kMinimumWidth, maxWidth); + size = constraints.constrain(Size(width ?? maxWidth, maxHeight!)); + } + + @override + void paint(PaintingContext context, Offset offset) { + final RenderBox? child = firstChild; + if (child != null) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + context.paintChild(child, offset + childParentData.offset); + } + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + final BoxConstraints constraints = this.constraints; + double maxWidth = 0.0; + double? maxHeight; + RenderBox? child = firstChild; + final BoxConstraints innerConstraints = BoxConstraints( + maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), + maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), + ); + + while (child != null) { + if (child == firstChild) { + final Size childSize = child.getDryLayout(innerConstraints); + maxHeight ??= childSize.height; + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + continue; + } + final Size childSize = child.getDryLayout(innerConstraints); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + childParentData.offset = Offset.zero; + maxWidth = math.max(maxWidth, childSize.width); + maxHeight ??= childSize.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + + assert(maxHeight != null); + maxWidth = math.max(_kMinimumWidth, maxWidth); + return constraints.constrain(Size(width ?? maxWidth, maxHeight!)); + } + + @override + double computeMinIntrinsicWidth(double height) { + RenderBox? child = firstChild; + double width = 0; + while (child != null) { + if (child == firstChild) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + continue; + } + final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); + if (child == lastChild) { + width += maxIntrinsicWidth; + } + if (child == childBefore(lastChild!)) { + width += maxIntrinsicWidth; + } + width = math.max(width, maxIntrinsicWidth); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + } + + return math.max(width, _kMinimumWidth); + } + + @override + double computeMaxIntrinsicWidth(double height) { + RenderBox? child = firstChild; + double width = 0; + while (child != null) { + if (child == firstChild) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + continue; + } + final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); + // Add the width of leading Icon. + if (child == lastChild) { + width += maxIntrinsicWidth; + } + // Add the width of trailing Icon. + if (child == childBefore(lastChild!)) { + width += maxIntrinsicWidth; + } + width = math.max(width, maxIntrinsicWidth); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + } + + return math.max(width, _kMinimumWidth); + } + + @override + double computeMinIntrinsicHeight(double height) { + final RenderBox? child = firstChild; + double width = 0; + if (child != null) { + width = math.max(width, child.getMinIntrinsicHeight(height)); + } + return width; + } + + @override + double computeMaxIntrinsicHeight(double height) { + final RenderBox? child = firstChild; + double width = 0; + if (child != null) { + width = math.max(width, child.getMaxIntrinsicHeight(height)); + } + return width; + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + final RenderBox? child = firstChild; + if (child != null) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + final bool isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - childParentData.offset); + return child.hitTest(result, position: transformed); + }, + ); + if (isHit) { + return true; + } + } + return false; + } +} + +// Hand coded defaults. These will be updated once we have tokens/spec. +class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { + _DropdownMenuDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + + @override + TextStyle? get textStyle => _theme.textTheme.bodyLarge; + + @override + MenuStyle get menuStyle { + return const MenuStyle( + minimumSize: MaterialStatePropertyAll(Size(_kMinimumWidth, 0.0)), + maximumSize: MaterialStatePropertyAll(Size.infinite), + visualDensity: VisualDensity.standard, + ); + } + + @override + InputDecorationTheme get inputDecorationTheme { + return const InputDecorationTheme(border: OutlineInputBorder()); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index d783fbde6d4a..53bd6cf6590b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -16,13 +16,11 @@ class WorkspaceSettingsBloc (event, emit) async { await event.when( initial: (userProfile, workspace) async { - UserWorkspacePB? currentWorkspace = workspace; - if (workspace == null) { - currentWorkspace = await _getWorkspace(userProfile.workspaceId); - } + final currentWorkspace = + workspace ?? await _getWorkspace(userProfile.workspaceId); // We emit here because the next event might take longer. - emit(state.copyWith(workspace: currentWorkspace!)); + emit(state.copyWith(workspace: currentWorkspace)); final members = await _getWorkspaceMembers(userProfile.workspaceId); final role = members diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 1daac0d4ffda..100b8d609988 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -46,7 +46,7 @@ class _WorkspaceIconState extends State { height: max(widget.iconSize, 26), decoration: BoxDecoration( color: ColorGenerator(widget.workspace.name).toColor(), - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(8), ), child: FlowyText( widget.workspace.name.isEmpty diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 1c07d3a677e3..3dc3f7caea66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -3,14 +3,19 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -22,7 +27,9 @@ import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.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/dialog/styled_dialogs.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; class SettingsWorkspaceView extends StatefulWidget { const SettingsWorkspaceView({ @@ -124,10 +131,10 @@ class _SettingsWorkspaceViewState extends State { ? 46 : 20, enableEdit: true, - onSelected: (emojiResult) => + onSelected: (result) => context.read().add( WorkspaceSettingsEvent.updateWorkspaceIcon( - emojiResult.emoji, + result.emoji, ), ), ), @@ -155,12 +162,57 @@ class _SettingsWorkspaceViewState extends State { const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_workspaceFont_title.tr(), - children: const [], + children: const [ + _FontSelectorDropdown(), + ], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_textDirection_title.tr(), - children: const [], + children: [ + BlocBuilder( + builder: (context, state) { + // TODO(Lucas): Do we even use TextDirection or do we just rely on LayoutDirection? + // Also if we rely on LayoutDirection, auto does not exist, but we can implement it using + // Bidi.isRtlLanguage(language) from Intl package. + return SettingsRadioSelect( + onChanged: (item) => context + .read() + .setTextDirection(item.value), + items: [ + SettingsRadioItem( + value: AppFlowyTextDirection.ltr, + icon: const FlowySvg(FlowySvgs.textdirection_ltr_m), + label: LocaleKeys + .settings_workspace_textDirection_leftToRight + .tr(), + isSelected: state.textDirection == + AppFlowyTextDirection.ltr, + ), + SettingsRadioItem( + value: AppFlowyTextDirection.rtl, + icon: const FlowySvg(FlowySvgs.textdirection_rtl_m), + label: LocaleKeys + .settings_workspace_textDirection_rightToLeft + .tr(), + isSelected: state.textDirection == + AppFlowyTextDirection.rtl, + ), + SettingsRadioItem( + value: AppFlowyTextDirection.auto, + icon: + const FlowySvg(FlowySvgs.textdirection_auto_m), + label: LocaleKeys + .settings_workspace_textDirection_auto + .tr(), + isSelected: state.textDirection == + AppFlowyTextDirection.auto, + ), + ], + ); + }, + ), + ], ), const SettingsCategorySpacer(), SettingsCategory( @@ -181,9 +233,7 @@ class _SettingsWorkspaceViewState extends State { } class _ThemeDropdown extends StatelessWidget { - const _ThemeDropdown({ - super.key, - }); + const _ThemeDropdown(); @override Widget build(BuildContext context) { @@ -192,8 +242,8 @@ class _ThemeDropdown extends StatelessWidget { child: BlocBuilder( buildWhen: (previous, current) => current is Ready, builder: (context, state) { - final currentTheme = - context.read().state.appTheme.themeName; + final appearance = context.watch().state; + final isLightMode = Theme.of(context).brightness == Brightness.light; final customThemes = state.maybeWhen( ready: (plugins) => @@ -202,37 +252,81 @@ class _ThemeDropdown extends StatelessWidget { ); return SettingsDropdown( - onChanged: (appTheme) => - context.read().setTheme(appTheme), - selectedOption: currentTheme, + actions: [ + SettingAction( + onPressed: () => Dialogs.show( + context, + child: BlocProvider.value( + value: context.read(), + child: const FlowyDialog( + constraints: BoxConstraints(maxHeight: 300), + child: ThemeUploadWidget(), + ), + ), + ).then((val) { + if (val != null) { + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_themeUpload_uploadSuccess + .tr(), + ); + } + }), + icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(16)), + ), + SettingAction( + onPressed: () => context + .read() + .setTheme(AppTheme.builtins.first.themeName), + icon: const FlowySvg(FlowySvgs.restore_s), + label: LocaleKeys.settings_common_reset.tr(), + ), + ], + onChanged: (theme) => + context.read().setTheme(theme), + selectedOption: appearance.appTheme.themeName, options: [ ...AppTheme.builtins.map( - (e) => DropdownMenuEntry( - value: e.themeName, - label: e.themeName, - ), + (t) { + final theme = isLightMode ? t.lightTheme : t.darkTheme; + + return buildDropdownMenuEntry( + context, + selectedValue: appearance.appTheme.themeName, + value: t.themeName, + label: t.themeName, + leadingWidget: _ThemeLeading(color: theme.sidebarBg), + ); + }, ), ...?customThemes?.map( - (e) => DropdownMenuEntry( - value: e.themeName, - label: e.themeName, - trailingIcon: FlowyIconButton( - onPressed: () { - context.read().add( - DynamicPluginEvent.removePlugin( - name: e.themeName, - ), - ); + (t) { + final theme = isLightMode ? t.lightTheme : t.darkTheme; - if (currentTheme == e.themeName) { - context.read().setTheme( - AppTheme.builtins.first.themeName, + return buildDropdownMenuEntry( + context, + selectedValue: appearance.appTheme.themeName, + value: t.themeName, + label: t.themeName, + leadingWidget: _ThemeLeading(color: theme.sidebarBg), + trailingWidget: FlowyIconButton( + icon: const FlowySvg(FlowySvgs.delete_s), + onPressed: () { + context.read().add( + DynamicPluginEvent.removePlugin( + name: t.themeName, + ), ); - } - }, - icon: const FlowySvg(FlowySvgs.delete_s), - ), - ), + + if (appearance.appTheme.themeName == t.themeName) { + context + .read() + .setTheme(AppTheme.builtins.first.themeName); + } + }, + ), + ); + }, ), ], ); @@ -242,6 +336,66 @@ class _ThemeDropdown extends StatelessWidget { } } +class SettingAction extends StatelessWidget { + const SettingAction({ + super.key, + required this.onPressed, + required this.icon, + this.label, + }); + + final VoidCallback onPressed; + final Widget icon; + final String? label; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onPressed, + child: SizedBox( + height: 26, + child: FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + children: [ + icon, + if (label != null) ...[ + const HSpace(4), + FlowyText.regular(label!), + ], + ], + ), + ), + ), + ), + ); + } +} + +class _ThemeLeading extends StatelessWidget { + const _ThemeLeading({required this.color}); + + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s4Border, + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + ), + ); + } +} + class _AppearanceSelector extends StatelessWidget { const _AppearanceSelector(); @@ -308,3 +462,52 @@ class _AppearanceSelector extends StatelessWidget { LocaleKeys.settings_workspace_appearance_options_dark.tr(), }; } + +class _FontSelectorDropdown extends StatelessWidget { + const _FontSelectorDropdown(); + + @override + Widget build(BuildContext context) { + final appearance = context.watch().state; + + return SettingsDropdown( + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .setFontFamily(builtInFontFamily), + child: SizedBox( + height: 26, + child: FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + children: [ + const FlowySvg(FlowySvgs.restore_s), + const HSpace(4), + FlowyText.regular(LocaleKeys.settings_common_reset.tr()), + ], + ), + ), + ), + ), + ), + ], + onChanged: (font) => + context.read().setFontFamily(font), + selectedOption: appearance.font, + options: [ + ...GoogleFonts.asMap().keys.toList().map( + (f) => buildDropdownMenuEntry( + context, + selectedValue: appearance.font, + value: f, + label: f, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart new file mode 100644 index 000000000000..9f90621b71cb --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +DropdownMenuEntry buildDropdownMenuEntry( + BuildContext context, { + required T value, + required String label, + T? selectedValue, + Widget? leadingWidget, + Widget? trailingWidget, +}) { + return DropdownMenuEntry( + style: ButtonStyle( + foregroundColor: MaterialStatePropertyAll( + AFThemeExtension.of(context).tint9, + ), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + ), + minimumSize: const MaterialStatePropertyAll( + Size(double.infinity, 29), + ), + maximumSize: const MaterialStatePropertyAll( + Size(double.infinity, 29), + ), + ), + value: value, + label: label, + leadingIcon: leadingWidget, + labelWidget: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: FlowyText.medium( + label, + fontSize: 14, + textAlign: TextAlign.start, + ), + ), + trailingIcon: Row( + children: [ + if (trailingWidget != null) ...[ + trailingWidget, + const HSpace(8), + ], + value == selectedValue + ? const FlowySvg(FlowySvgs.check_s) + : const SizedBox.shrink(), + ], + ), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart index 12abb1699be9..a5429ff9a905 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart @@ -34,6 +34,10 @@ class SettingsActionableInput extends StatelessWidget { isDense: false, suffixIconConstraints: BoxConstraints.tight(const Size(23 + 18, 24)), + textStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), onSubmitted: onSave, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 84b49de8def8..ddee59a8f411 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:appflowy/flutter/af_dropdown_menu.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; class SettingsDropdown extends StatefulWidget { @@ -33,19 +35,66 @@ class _SettingsDropdownState extends State { Widget build(BuildContext context) { return Row( children: [ - DropdownMenu( - controller: controller, - menuStyle: - MenuStyle(visualDensity: VisualDensity.adaptivePlatformDensity), - initialSelection: widget.selectedOption, - dropdownMenuEntries: widget.options, - onSelected: (value) => - value != null ? widget.onChanged?.call(value) : null, + Expanded( + child: AFDropdownMenu( + controller: controller, + expandedInsets: EdgeInsets.zero, + initialSelection: widget.selectedOption, + dropdownMenuEntries: widget.options, + menuStyle: MenuStyle( + maximumSize: + const MaterialStatePropertyAll(Size(double.infinity, 250)), + elevation: const MaterialStatePropertyAll(10), + shadowColor: + MaterialStatePropertyAll(Colors.black.withOpacity(0.4)), + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.surface, + ), + padding: const MaterialStatePropertyAll( + EdgeInsets.symmetric(horizontal: 6, vertical: 8), + ), + alignment: Alignment.bottomLeft, + visualDensity: VisualDensity.compact, + ), + inputDecorationTheme: InputDecorationTheme( + contentPadding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 18, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: Corners.s8Border, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: Corners.s8Border, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s8Border, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s8Border, + ), + ), + onSelected: (v) async { + v != null ? widget.onChanged?.call(v) : null; + }, + ), ), if (widget.actions?.isNotEmpty == true) ...[ const HSpace(16), SeparatedRow( - separatorBuilder: () => const HSpace(16), + separatorBuilder: () => const HSpace(8), children: widget.actions!, ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart new file mode 100644 index 000000000000..91d780cedadc --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class SettingsRadioItem { + const SettingsRadioItem({ + required this.value, + required this.label, + required this.isSelected, + this.icon, + }); + + final T value; + final String label; + final bool isSelected; + final Widget? icon; +} + +class SettingsRadioSelect extends StatelessWidget { + const SettingsRadioSelect({ + super.key, + required this.items, + required this.onChanged, + this.selectedItem, + }); + + final List> items; + final void Function(SettingsRadioItem) onChanged; + final SettingsRadioItem? selectedItem; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 24, + runSpacing: 8, + children: items + .map( + (i) => GestureDetector( + onTap: () => onChanged(i), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 14, + height: 14, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AFThemeExtension.of(context).textColor, + ), + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: i.isSelected + ? AFThemeExtension.of(context).textColor + : Colors.transparent, + shape: BoxShape.circle, + ), + ), + ), + const HSpace(8), + if (i.icon != null) ...[i.icon!, const HSpace(4)], + FlowyText.regular(i.label, fontSize: 14), + ], + ), + ), + ) + .toList(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart index bf91f9209753..d20a5e0309f6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -12,7 +14,6 @@ import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ColorSchemeSetting extends StatelessWidget { @@ -64,13 +65,15 @@ class ColorSchemeUploadOverlayButton extends StatelessWidget { child: ThemeUploadWidget(), ), ), - ).then((value) { - if (value == null) return; - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(), - ); - }), + ).then( + (value) { + if (value == null) return; + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(), + ); + }, + ), ); } } diff --git a/frontend/resources/flowy_icons/24x/textdirection_auto.svg b/frontend/resources/flowy_icons/24x/textdirection_auto.svg new file mode 100644 index 000000000000..aed628978731 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/textdirection_auto.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/textdirection_ltr.svg b/frontend/resources/flowy_icons/24x/textdirection_ltr.svg new file mode 100644 index 000000000000..9fc05cf0baf3 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/textdirection_ltr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/textdirection_rtl.svg b/frontend/resources/flowy_icons/24x/textdirection_rtl.svg new file mode 100644 index 000000000000..62d87cef7701 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/textdirection_rtl.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 444e28c9fbea..01055481c817 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -354,7 +354,10 @@ "title": "Workspace font" }, "textDirection": { - "title": "Text direction" + "title": "Text direction", + "leftToRight": "Left to right", + "rightToLeft": "Right to left", + "auto": "Auto" }, "dateTime": { "title": "Date & time" @@ -363,6 +366,9 @@ "title": "Language" } }, + "common": { + "reset": "Reset" + }, "menu": { "appearance": "Appearance", "language": "Language", From 704977faa71ebe87f9a3da056ce7b4575dd61070 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 9 Apr 2024 22:39:10 +0200 Subject: [PATCH 13/16] chore: duplicate keys after merge --- frontend/resources/translations/en.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3173382b5d2a..9d93ce393dfa 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1004,8 +1004,7 @@ "newDatabase": "New Database", "linkToDatabase": "Link to Database" }, - "date": "Date", - "emoji": "Emoji" + "date": "Date" }, "outlineBlock": { "placeholder": "Table of Contents" From 017967e0d486e9249a4a6c5d0cd7b1a142b514d2 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Wed, 10 Apr 2024 18:14:40 +0200 Subject: [PATCH 14/16] feat: finish workspace page --- .../date/date_time_format.dart | 32 +- .../settings/date_time/time_format_ext.dart | 13 + .../pages/settings_workspace_view.dart | 314 +++++++++++++----- .../settings/shared/settings_category.dart | 1 + .../shared/settings_category_spacer.dart | 6 +- .../shared/settings_dotted_divider.dart | 75 +++++ .../settings/shared/settings_dropdown.dart | 27 +- frontend/resources/translations/en.json | 12 +- 8 files changed, 367 insertions(+), 113 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dotted_divider.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart index 4c7dc73ae200..fa72b41dc5c0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart @@ -1,3 +1,5 @@ +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/database/grid/presentation/layout/sizes.dart'; @@ -6,7 +8,6 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class DateFormatButton extends StatelessWidget { const DateFormatButton({ @@ -165,13 +166,9 @@ class TimeFormatList extends StatelessWidget { width: 120, child: ListView.separated( shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, itemCount: cells.length, - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, + separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), + itemBuilder: (_, int index) => cells[index], ), ); } @@ -208,27 +205,24 @@ class TimeFormatCell extends StatelessWidget { } extension TimeFormatExtension on TimeFormatPB { - String title() { - switch (this) { - case TimeFormatPB.TwelveHour: - return LocaleKeys.grid_field_timeFormatTwelveHour.tr(); - case TimeFormatPB.TwentyFourHour: - return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(); - default: - throw UnimplementedError; - } - } + String title() => switch (this) { + TimeFormatPB.TwelveHour => + LocaleKeys.grid_field_timeFormatTwelveHour.tr(), + TimeFormatPB.TwentyFourHour => + LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(), + _ => throw UnimplementedError(), + }; } class IncludeTimeButton extends StatelessWidget { const IncludeTimeButton({ super.key, - required this.onChanged, required this.value, + required this.onChanged, }); - final Function(bool value) onChanged; final bool value; + final Function(bool value) onChanged; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart new file mode 100644 index 000000000000..5da3caa5b999 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension TimeFormatter on UserTimeFormatPB { + DateFormat get toFormat => _toFormat[this]!; + + String formatTime(DateTime date) => toFormat.format(date); +} + +final _toFormat = { + UserTimeFormatPB.TwelveHour: DateFormat.Hm(), + UserTimeFormatPB.TwentyFourHour: DateFormat.jm(), +}; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 3dc3f7caea66..65edcaedc652 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -4,6 +4,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; +import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart'; import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -12,13 +14,18 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_actiona import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dotted_divider.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; @@ -114,115 +121,49 @@ class _SettingsWorkspaceViewState extends State { .settings_workspace_workspaceIcon_description .tr(), children: [ - Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: BorderRadius.circular(8), - ), - height: 64, - width: 64, - child: Padding( - padding: const EdgeInsets.all(1), - child: WorkspaceIcon( - workspace: state.workspace!, - iconSize: state.workspace?.icon.isNotEmpty == true - ? 46 - : 20, - enableEdit: true, - onSelected: (result) => - context.read().add( - WorkspaceSettingsEvent.updateWorkspaceIcon( - result.emoji, - ), - ), - ), - ), - ), + if (state.workspace != null) + _WorkspaceIconSetting(workspace: state.workspace!), ], ), ], const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_appearance_title.tr(), - children: const [ - _AppearanceSelector(), - ], + children: const [_AppearanceSelector()], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_theme_title.tr(), description: LocaleKeys.settings_workspace_theme_description.tr(), - children: const [ - _ThemeDropdown(), - ], + children: const [_ThemeDropdown()], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_workspaceFont_title.tr(), - children: const [ - _FontSelectorDropdown(), - ], + children: const [_FontSelectorDropdown()], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_textDirection_title.tr(), - children: [ - BlocBuilder( - builder: (context, state) { - // TODO(Lucas): Do we even use TextDirection or do we just rely on LayoutDirection? - // Also if we rely on LayoutDirection, auto does not exist, but we can implement it using - // Bidi.isRtlLanguage(language) from Intl package. - return SettingsRadioSelect( - onChanged: (item) => context - .read() - .setTextDirection(item.value), - items: [ - SettingsRadioItem( - value: AppFlowyTextDirection.ltr, - icon: const FlowySvg(FlowySvgs.textdirection_ltr_m), - label: LocaleKeys - .settings_workspace_textDirection_leftToRight - .tr(), - isSelected: state.textDirection == - AppFlowyTextDirection.ltr, - ), - SettingsRadioItem( - value: AppFlowyTextDirection.rtl, - icon: const FlowySvg(FlowySvgs.textdirection_rtl_m), - label: LocaleKeys - .settings_workspace_textDirection_rightToLeft - .tr(), - isSelected: state.textDirection == - AppFlowyTextDirection.rtl, - ), - SettingsRadioItem( - value: AppFlowyTextDirection.auto, - icon: - const FlowySvg(FlowySvgs.textdirection_auto_m), - label: LocaleKeys - .settings_workspace_textDirection_auto - .tr(), - isSelected: state.textDirection == - AppFlowyTextDirection.auto, - ), - ], - ); - }, - ), - ], + children: const [_TextDirectionSelect()], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_dateTime_title.tr(), - children: const [], + children: [ + const _DateTimeFormatLabel(), + const _TimeFormatSwitcher(), + SettingsDashedDivider( + color: Theme.of(context).colorScheme.outline, + ), + const _DateFormatDropdown(), + ], ), const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspace_language_title.tr(), - children: const [], + children: const [_LanguageDropdown()], ), ], ); @@ -232,6 +173,217 @@ class _SettingsWorkspaceViewState extends State { } } +class _LanguageDropdown extends StatelessWidget { + const _LanguageDropdown(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SettingsDropdown( + expandWidth: false, + onChanged: (locale) => context + .read() + .setLocale(context, locale), + selectedOption: state.locale, + options: EasyLocalization.of(context)! + .supportedLocales + .map( + (locale) => buildDropdownMenuEntry( + context, + selectedValue: state.locale, + value: locale, + label: languageFromLocale(locale), + ), + ) + .toList(), + ); + }, + ); + } +} + +class _WorkspaceIconSetting extends StatelessWidget { + const _WorkspaceIconSetting({required this.workspace}); + + final UserWorkspacePB workspace; + + @override + Widget build(BuildContext context) { + return Container( + height: 64, + width: 64, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(1), + child: WorkspaceIcon( + workspace: workspace, + iconSize: workspace.icon.isNotEmpty == true ? 46 : 20, + enableEdit: true, + onSelected: (r) => context + .read() + .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), + ), + ), + ); + } +} + +class _TextDirectionSelect extends StatelessWidget { + const _TextDirectionSelect(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + // TODO(Lucas): Do we even use TextDirection or do we just rely on LayoutDirection? + // Also if we rely on LayoutDirection, auto does not exist, but we can implement it using + // Bidi.isRtlLanguage(language) from Intl package. + return SettingsRadioSelect( + onChanged: (item) => context + .read() + .setTextDirection(item.value), + items: [ + SettingsRadioItem( + value: AppFlowyTextDirection.ltr, + icon: const FlowySvg(FlowySvgs.textdirection_ltr_m), + label: + LocaleKeys.settings_workspace_textDirection_leftToRight.tr(), + isSelected: state.textDirection == AppFlowyTextDirection.ltr, + ), + SettingsRadioItem( + value: AppFlowyTextDirection.rtl, + icon: const FlowySvg(FlowySvgs.textdirection_rtl_m), + label: + LocaleKeys.settings_workspace_textDirection_rightToLeft.tr(), + isSelected: state.textDirection == AppFlowyTextDirection.rtl, + ), + SettingsRadioItem( + value: AppFlowyTextDirection.auto, + icon: const FlowySvg(FlowySvgs.textdirection_auto_m), + label: LocaleKeys.settings_workspace_textDirection_auto.tr(), + isSelected: state.textDirection == AppFlowyTextDirection.auto, + ), + ], + ); + }, + ); + } +} + +class _DateFormatDropdown extends StatelessWidget { + const _DateFormatDropdown(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + LocaleKeys.settings_workspace_dateTime_dateFormat_label.tr(), + fontSize: 16, + ), + const VSpace(8), + SettingsDropdown( + expandWidth: false, + onChanged: (format) => context + .read() + .setDateFormat(format), + selectedOption: state.dateFormat, + options: UserDateFormatPB.values + .map( + (f) => buildDropdownMenuEntry( + context, + value: f, + label: _formatLabel(f), + ), + ) + .toList(), + ), + ], + ), + ); + }, + ); + } + + String _formatLabel(UserDateFormatPB format) => switch (format) { + UserDateFormatPB.Locally => + LocaleKeys.settings_workspace_dateTime_dateFormat_local.tr(), + UserDateFormatPB.US => + LocaleKeys.settings_workspace_dateTime_dateFormat_us.tr(), + UserDateFormatPB.ISO => + LocaleKeys.settings_workspace_dateTime_dateFormat_iso.tr(), + UserDateFormatPB.Friendly => + LocaleKeys.settings_workspace_dateTime_dateFormat_friendly.tr(), + UserDateFormatPB.DayMonthYear => + LocaleKeys.settings_workspace_dateTime_dateFormat_dmy.tr(), + _ => "Unknown format", + }; +} + +class _DateTimeFormatLabel extends StatelessWidget { + const _DateTimeFormatLabel(); + + @override + Widget build(BuildContext context) { + final now = DateTime.now(); + + return BlocBuilder( + builder: (context, state) { + return FlowyText.regular( + LocaleKeys.settings_workspace_dateTime_example.tr( + args: [ + state.dateFormat.formatDate(now, false), + state.timeFormat.formatTime(now), + now.timeZoneName, + ], + ), + fontSize: 16, + color: AFThemeExtension.of(context).secondaryTextColor, + ); + }, + ); + } +} + +class _TimeFormatSwitcher extends StatelessWidget { + const _TimeFormatSwitcher(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.regular( + LocaleKeys.settings_workspace_dateTime_24HourTime.tr(), + fontSize: 16, + ), + ), + const HSpace(16), + Toggle( + style: ToggleStyle.big, + value: context.watch().state.timeFormat == + UserTimeFormatPB.TwentyFourHour, + onChanged: (value) => + context.read().setTimeFormat( + value + ? UserTimeFormatPB.TwelveHour + : UserTimeFormatPB.TwentyFourHour, + ), + ), + ], + ); + } +} + class _ThemeDropdown extends StatelessWidget { const _ThemeDropdown(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index 3dfc34828a04..84ceae185acc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -41,6 +41,7 @@ class SettingsCategory extends StatelessWidget { ], SeparatedColumn( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, separatorBuilder: () => children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), children: children, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index 2ef45eadad55..8b86759f9eda 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme_extension.dart'; + /// This is used to create a uniform space and divider /// between categories in settings. /// @@ -8,9 +10,9 @@ class SettingsCategorySpacer extends StatelessWidget { @override Widget build(BuildContext context) { - return const Divider( + return Divider( height: 32, - color: Color(0xFFF2F2F2), + color: AFThemeExtension.of(context).toggleOffFill, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dotted_divider.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dotted_divider.dart new file mode 100644 index 000000000000..6a8405dc56dd --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dotted_divider.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +/// Renders a dashed divider +/// +/// The length of each dash is the same as the gap. +/// +class SettingsDashedDivider extends StatelessWidget { + const SettingsDashedDivider({ + super.key, + this.color, + this.height, + this.strokeWidth = 1.0, + this.gap = 3.0, + this.direction = Axis.horizontal, + }); + + // The color of the divider, defaults to the theme's divider color + final Color? color; + + // The height of the divider, this will surround the divider equally + final double? height; + + // Thickness of the divider + final double strokeWidth; + + // Gap between the dashes + final double gap; + + // Direction of the divider + final Axis direction; + + @override + Widget build(BuildContext context) { + final double padding = + height != null && height! > 0 ? (height! - strokeWidth) / 2 : 0; + + return LayoutBuilder( + builder: (context, constraints) { + final items = _calculateItems(constraints); + return Padding( + padding: EdgeInsets.symmetric( + vertical: direction == Axis.horizontal ? padding : 0, + horizontal: direction == Axis.vertical ? padding : 0, + ), + child: Wrap( + direction: direction, + children: List.generate( + items, + (index) => Container( + margin: EdgeInsets.only( + right: direction == Axis.horizontal ? gap : 0, + bottom: direction == Axis.vertical ? gap : 0, + ), + width: direction == Axis.horizontal ? gap : strokeWidth, + height: direction == Axis.vertical ? gap : strokeWidth, + decoration: BoxDecoration( + color: color ?? Theme.of(context).dividerColor, + borderRadius: BorderRadius.circular(1.0), + ), + ), + ), + ), + ); + }, + ); + } + + int _calculateItems(BoxConstraints constraints) { + final double totalLength = direction == Axis.horizontal + ? constraints.maxWidth + : constraints.maxHeight; + + return (totalLength / (gap * 2)).floor(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index ddee59a8f411..a431658911ed 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -4,41 +4,47 @@ import 'package:appflowy/flutter/af_dropdown_menu.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -class SettingsDropdown extends StatefulWidget { +class SettingsDropdown extends StatefulWidget { const SettingsDropdown({ super.key, required this.selectedOption, required this.options, this.onChanged, this.actions, + this.expandWidth = true, }); - final String selectedOption; - final List> options; - final void Function(String)? onChanged; + final T selectedOption; + final List> options; + final void Function(T)? onChanged; final List? actions; + final bool expandWidth; @override - State createState() => _SettingsDropdownState(); + State> createState() => _SettingsDropdownState(); } -class _SettingsDropdownState extends State { +class _SettingsDropdownState extends State> { late final TextEditingController controller; @override void initState() { super.initState(); - controller = TextEditingController(text: widget.selectedOption); + controller = TextEditingController( + text: widget.options + .firstWhere((e) => e.value == widget.selectedOption) + .label, + ); } @override Widget build(BuildContext context) { return Row( children: [ - Expanded( - child: AFDropdownMenu( + Flexible( + child: AFDropdownMenu( controller: controller, - expandedInsets: EdgeInsets.zero, + expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, menuStyle: MenuStyle( @@ -87,6 +93,7 @@ class _SettingsDropdownState extends State { ), ), onSelected: (v) async { + debugPrint("REACHED HERE ($v)"); v != null ? widget.onChanged?.call(v) : null; }, ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9d93ce393dfa..3413b15211e2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -361,7 +361,17 @@ "auto": "Auto" }, "dateTime": { - "title": "Date & time" + "title": "Date & time", + "example": "{} at {} ({})", + "24HourTime": "24-hour time", + "dateFormat": { + "label": "Date format", + "local": "Local", + "us": "US", + "iso": "ISO", + "friendly": "Friendly", + "dmy": "D/M/Y" + } }, "language": { "title": "Language" From 9b5c0faa8e8fdad71febd866b66e17e85d7a1844 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Wed, 10 Apr 2024 23:11:29 +0200 Subject: [PATCH 15/16] feat: leave/delete workspace initial --- .../workspace/workspace_settings_bloc.dart | 72 +++++++++++++++++-- .../pages/settings_workspace_view.dart | 63 +++++++++++++--- .../shared/settings_alert_dialog.dart | 1 + .../shared/single_setting_action.dart | 6 +- frontend/resources/translations/en.json | 13 ++++ 5 files changed, 141 insertions(+), 14 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index 53bd6cf6590b..e8b4135fd6c8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -1,3 +1,9 @@ +import 'package:flutter/foundation.dart'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -16,13 +22,29 @@ class WorkspaceSettingsBloc (event, emit) async { await event.when( initial: (userProfile, workspace) async { - final currentWorkspace = - workspace ?? await _getWorkspace(userProfile.workspaceId); + _userService = UserBackendService(userId: userProfile.id); + + late UserWorkspacePB currentWorkspace; + final kvStore = getIt(); + final lastOpenedWorkspaceId = + await kvStore.get(KVKeys.lastOpenedWorkspaceId); + + if (lastOpenedWorkspaceId != null) { + currentWorkspace = await _getWorkspace(lastOpenedWorkspaceId); + } else { + currentWorkspace = await _getWorkspace(userProfile.workspaceId); + await kvStore.set( + KVKeys.lastOpenedWorkspaceId, + userProfile.workspaceId, + ); + } // We emit here because the next event might take longer. emit(state.copyWith(workspace: currentWorkspace)); - final members = await _getWorkspaceMembers(userProfile.workspaceId); + final members = await _getWorkspaceMembers( + currentWorkspace.workspaceId, + ); final role = members .firstWhereOrNull((e) => e.email == userProfile.email) ?.role ?? @@ -70,14 +92,55 @@ class WorkspaceSettingsBloc (e) => Log.error('Failed to update workspace icon: $e'), ); }, + deleteWorkspace: () async { + final request = + UserWorkspaceIdPB(workspaceId: state.workspace!.workspaceId); + final result = await UserEventDeleteWorkspace(request).send(); + + await result.fold( + (_) async { + final workspaces = await _userService?.getWorkspaces(); + final workspace = workspaces?.toNullable()?.first; + if (workspace != null) { + await getIt().set( + KVKeys.lastOpenedWorkspaceId, + workspace.workspaceId, + ); + emit(state.copyWith(workspace: workspace)); + } + }, + (f) async => Log.error('Failed to delete workspace $f'), + ); + }, addWorkspaceMember: (email) {}, removeWorkspaceMember: (email) {}, updateWorkspaceMember: (email, role) {}, + leaveWorkspace: () async { + final result = await _userService + ?.leaveWorkspace(state.workspace!.workspaceId); + + await result?.fold( + (_) async { + final workspaces = await _userService?.getWorkspaces(); + final workspace = workspaces?.toNullable()?.first; + if (workspace != null) { + await getIt().set( + KVKeys.lastOpenedWorkspaceId, + workspace.workspaceId, + ); + emit(state.copyWith(workspace: workspace)); + } + }, + (f) async => Log.error('Failed to leave workspace: $f'), + ); + }, ); }, ); } + UserBackendService? _userService; + Future _getWorkspace(String workspaceId) async { final request = UserWorkspaceIdPB(workspaceId: workspaceId); final result = await UserEventGetWorkspace(request).send(); @@ -115,9 +178,9 @@ class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent { // Workspace itself const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) = UpdateWorkspaceName; - const factory WorkspaceSettingsEvent.updateWorkspaceIcon(String icon) = UpdateWorkspaceIcon; + const factory WorkspaceSettingsEvent.deleteWorkspace() = DeleteWorkspace; // Workspace Member const factory WorkspaceSettingsEvent.addWorkspaceMember(String email) = @@ -128,6 +191,7 @@ class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent { String email, AFRolePB role, ) = UpdateWorkspaceMember; + const factory WorkspaceSettingsEvent.leaveWorkspace() = leaveWorkspace; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 65edcaedc652..287f709ca99d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; @@ -11,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; @@ -18,12 +21,11 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_dotted_ import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; @@ -67,10 +69,16 @@ class _SettingsWorkspaceViewState extends State { ..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)), child: BlocConsumer( listenWhen: (previous, current) => - previous.workspace?.name != current.workspace?.name, - listener: (context, state) => - _workspaceNameController.text = state.workspace?.name ?? '', + previous.workspace?.workspaceId != current.workspace?.workspaceId && + previous.workspace?.workspaceId != null, + listener: (context, state) async { + // TODO(Mathias): Temporary measure, remember to fix to set the + // current workspace upon deleting/leaving instead of this hack. + Navigator.of(context).pop(); + await runAppFlowy(); + }, builder: (context, state) { + _workspaceNameController.text = state.workspace?.name ?? ''; return SettingsBody( children: [ SettingsHeader( @@ -161,9 +169,46 @@ class _SettingsWorkspaceViewState extends State { ], ), const SettingsCategorySpacer(), - SettingsCategory( - title: LocaleKeys.settings_workspace_language_title.tr(), - children: const [_LanguageDropdown()], + FlowyText.regular( + LocaleKeys.settings_workspace_language_title.tr(), + fontSize: 16, + ), + const VSpace(8), + const _LanguageDropdown(), + const SettingsCategorySpacer(), + SingleSettingAction( + label: LocaleKeys.settings_workspace_manageWorkspace_title.tr(), + fontSize: 16, + fontWeight: FontWeight.w600, + onPressed: () => SettingsAlertDialog( + title: state.myRole.isOwner + ? LocaleKeys + .settings_workspace_deleteWorkspacePrompt_title + .tr() + : LocaleKeys.settings_workspace_leaveWorkspacePrompt_title + .tr(), + subtitle: state.myRole.isOwner + ? LocaleKeys + .settings_workspace_deleteWorkspacePrompt_content + .tr() + : LocaleKeys + .settings_workspace_leaveWorkspacePrompt_content + .tr(), + isDangerous: true, + confirm: () => context.read().add( + state.myRole.isOwner + ? const WorkspaceSettingsEvent.deleteWorkspace() + : const WorkspaceSettingsEvent.leaveWorkspace(), + ), + ).show(context), + isDangerous: true, + buttonLabel: state.myRole.isOwner + ? LocaleKeys + .settings_workspace_manageWorkspace_deleteWorkspace + .tr() + : LocaleKeys + .settings_workspace_manageWorkspace_leaveWorkspace + .tr(), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart index bc3c7a8f2139..17d60359cd9e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart @@ -109,6 +109,7 @@ class _SettingsAlertDialogState extends State { widget.subtitle!, fontSize: 16, color: Theme.of(context).colorScheme.tertiary, + textAlign: TextAlign.center, maxLines: null, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart index 4fcd30e25d76..7b7dca4d177f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart @@ -22,6 +22,7 @@ class SingleSettingAction extends StatelessWidget { this.onPressed, this.isDangerous = false, this.fontSize = 14, + this.fontWeight = FontWeight.normal, }); final String label; @@ -41,14 +42,17 @@ class SingleSettingAction extends StatelessWidget { final double fontSize; + final FontWeight fontWeight; + @override Widget build(BuildContext context) { return Row( children: [ Expanded( - child: FlowyText.medium( + child: FlowyText( label, fontSize: fontSize, + fontWeight: fontWeight, maxLines: labelMaxLines, overflow: TextOverflow.ellipsis, color: AFThemeExtension.of(context).secondaryTextColor, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 81379d795b26..5136d7df5c0f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -375,6 +375,19 @@ }, "language": { "title": "Language" + }, + "deleteWorkspacePrompt": { + "title": "Delete workspace", + "content": "Are you sure you want to delete this workspace? This action cannot be undone." + }, + "leaveWorkspacePrompt": { + "title": "Leave workspace", + "content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it." + }, + "manageWorkspace": { + "title": "Manage workspace", + "leaveWorkspace": "Leave workspace", + "deleteWorkspace": "Delete workspace" } }, "common": { From 173f4bbf34f07104ee849fbdc9ed9de2293d267f Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Thu, 11 Apr 2024 13:11:14 +0200 Subject: [PATCH 16/16] feat: clean up and refactor members view --- .../settings/settings_dialog_bloc.dart | 3 +- .../pages/settings_manage_data_view.dart | 25 + .../settings/pages/settings_members_view.dart | 438 +++++++++++++++ .../settings/settings_dialog.dart | 8 +- .../members/workspace_member_bloc.dart | 8 +- .../members/workspace_member_page.dart | 504 ------------------ .../settings/widgets/settings_menu.dart | 24 +- frontend/resources/translations/en.json | 33 ++ 8 files changed, 521 insertions(+), 522 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_members_view.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index b29ab29766a3..ef7b6c84bfb5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -11,6 +11,8 @@ part 'settings_dialog_bloc.freezed.dart'; enum SettingsPage { account, workspace, + member, + manageData, // OLD appearance, language, @@ -19,7 +21,6 @@ enum SettingsPage { notifications, cloud, shortcuts, - member, featureFlags, } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart new file mode 100644 index 000000000000..1acf01dfa567 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class SettingsManageDataView extends StatelessWidget { + const SettingsManageDataView({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return SettingsBody( + children: [ + SettingsHeader( + title: LocaleKeys.settings_manageData_title.tr(), + description: LocaleKeys.settings_manageData_description.tr(), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_members_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_members_view.dart new file mode 100644 index 000000000000..8e61bace494e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_members_view.dart @@ -0,0 +1,438 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; + +class SettingsMembersView extends StatefulWidget { + const SettingsMembersView({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => _SettingsMembersViewState(); +} + +class _SettingsMembersViewState extends State { + final _inviteEmailController = TextEditingController(); + + @override + void dispose() { + _inviteEmailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WorkspaceMemberBloc(userProfile: widget.userProfile) + ..add(const WorkspaceMemberEvent.initial()), + child: BlocConsumer( + listener: _showResultDialog, + builder: (context, state) { + return SettingsBody( + children: [ + SettingsHeader( + title: LocaleKeys.settings_appearance_members_title.tr(), + ), + if (state.myRole.canInvite) + SettingsCategory( + title: + LocaleKeys.settings_appearance_members_inviteMembers.tr(), + children: [ + SettingsActionableInput( + controller: _inviteEmailController, + actions: [ + SizedBox( + height: 48, + child: FlowyTextButton( + LocaleKeys.settings_appearance_members_sendInvite + .tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () { + final email = _inviteEmailController.text; + if (!isEmail(email)) { + return showSnackBarMessage( + context, + LocaleKeys + .settings_appearance_members_emailInvalidError + .tr(), + ); + } + + context.read().add( + WorkspaceMemberEvent.addWorkspaceMember( + _inviteEmailController.text, + ), + ); + }, + ), + ), + ], + ), + /* Enable this when the feature is ready + PrimaryButton( + backgroundColor: const Color(0xFFE0E0E0), + child: Padding( + padding: const EdgeInsets.only( + left: 20, + right: 24, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.invite_member_link_m, + color: Colors.black, + ), + const HSpace(8.0), + FlowyText( + LocaleKeys.settings_appearance_members_copyInviteLink.tr(), + color: Colors.black, + ), + ], + ), + ), + onPressed: () { + showSnackBarMessage(context, 'not implemented'); + }, + ), + const VSpace(16.0), + */ + ], + ), + if (state.members.isNotEmpty) ...[ + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_appearance_members_label.tr(), + children: [ + _MemberList( + members: state.members, + myRole: state.myRole, + userProfile: widget.userProfile, + ), + ], + ), + ], + ], + ); + }, + ), + ); + } + + void _showResultDialog(BuildContext context, WorkspaceMemberState state) { + final actionResult = state.actionResult; + if (actionResult == null) { + return; + } + + final actionType = actionResult.actionType; + final result = actionResult.result; + + // only show the result dialog when the action is WorkspaceMemberActionType.add + if (actionType == WorkspaceMemberActionType.add) { + result.fold( + (_) => showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + ), + (error) => showDialog( + context: context, + builder: (_) => NavigatorOkCancelDialog( + message: error.code == ErrorCode.WorkspaceMemberLimitExceeded + ? LocaleKeys.settings_appearance_members_memberLimitExceeded + .tr() + : LocaleKeys.settings_appearance_members_failedToAddMember.tr(), + ), + ), + ); + } + + result.onFailure( + (f) => Log.error( + '[Member] Failed to perform ${actionType.toString()} action: $f', + ), + ); + } +} + +class _MemberList extends StatelessWidget { + const _MemberList({ + required this.members, + required this.myRole, + required this.userProfile, + }); + + final List members; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const VSpace(16.0), + SeparatedColumn( + crossAxisAlignment: CrossAxisAlignment.start, + separatorBuilder: () => const Divider(), + children: [ + const _MemberListHeader(), + ...members.map( + (member) => _MemberItem( + member: member, + myRole: myRole, + userProfile: userProfile, + ), + ), + ], + ), + ], + ); + } +} + +class _MemberListHeader extends StatelessWidget { + const _MemberListHeader(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_user.tr(), + fontSize: 14.0, + ), + ), + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_role.tr(), + fontSize: 14.0, + ), + ), + const HSpace(28.0), + ], + ); + } +} + +class _MemberItem extends StatelessWidget { + const _MemberItem({ + required this.member, + required this.myRole, + required this.userProfile, + }); + + final WorkspaceMemberPB member; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; + return Row( + children: [ + Expanded( + child: FlowyText.medium( + member.name, + color: textColor, + fontSize: 14.0, + ), + ), + Expanded( + child: member.role.isOwner || !myRole.canUpdate + ? FlowyText.medium( + member.role.description, + color: textColor, + fontSize: 14.0, + ) + : _MemberRoleActionList(member: member), + ), + myRole.canDelete && + member.email != userProfile.email // can't delete self + ? _MemberMoreActionList(member: member) + : const HSpace(28.0), + ], + ); + } +} + +enum _MemberMoreAction { delete } + +class _MemberMoreActionList extends StatelessWidget { + const _MemberMoreActionList({required this.member}); + + final WorkspaceMemberPB member; + + @override + Widget build(BuildContext context) { + return PopoverActionList<_MemberMoreActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithCenterAligned, + actions: _MemberMoreAction.values + .map((e) => _MemberMoreActionWrapper(e, member)) + .toList(), + buildChild: (controller) => FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.three_dots_vertical_s), + onTap: controller.show, + ), + onSelected: (action, controller) { + switch (action.inner) { + case _MemberMoreAction.delete: + showDialog( + context: context, + builder: (_) => NavigatorOkCancelDialog( + title: LocaleKeys.settings_appearance_members_removeMember.tr(), + message: LocaleKeys + .settings_appearance_members_areYouSureToRemoveMember + .tr(), + okTitle: LocaleKeys.button_yes.tr(), + onOkPressed: () => context.read().add( + WorkspaceMemberEvent.removeWorkspaceMember( + action.member.email, + ), + ), + ), + ); + break; + } + controller.close(); + }, + ); + } +} + +class _MemberMoreActionWrapper extends ActionCell { + _MemberMoreActionWrapper(this.inner, this.member); + + final _MemberMoreAction inner; + final WorkspaceMemberPB member; + + @override + String get name => switch (inner) { + _MemberMoreAction.delete => + LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(), + }; +} + +class _MemberRoleActionList extends StatelessWidget { + const _MemberRoleActionList({required this.member}); + + final WorkspaceMemberPB member; + + @override + Widget build(BuildContext context) { + return PopoverActionList<_MemberRoleActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithLeftAligned, + actions: [_MemberRoleActionWrapper(AFRolePB.Member, member)], + offset: const Offset(0, 10), + buildChild: (controller) => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: controller.show, + child: Row( + children: [ + FlowyText.medium(member.role.description, fontSize: 14.0), + const HSpace(8.0), + const FlowySvg(FlowySvgs.drop_menu_show_s), + ], + ), + ), + ), + onSelected: (action, controller) { + switch (action.inner) { + case AFRolePB.Member: + case AFRolePB.Guest: + context.read().add( + WorkspaceMemberEvent.updateWorkspaceMember( + action.member.email, + action.inner, + ), + ); + break; + case AFRolePB.Owner: + break; + } + controller.close(); + }, + ); + } +} + +class _MemberRoleActionWrapper extends ActionCell { + _MemberRoleActionWrapper(this.inner, this.member); + + final AFRolePB inner; + final WorkspaceMemberPB member; + + @override + Widget? rightIcon(Color iconColor) { + return SizedBox( + width: 58.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTooltip( + message: tooltip, + child: const FlowySvg(FlowySvgs.information_s), + ), + const Spacer(), + if (member.role == inner) const FlowySvg(FlowySvgs.checkmark_tiny_s), + ], + ), + ); + } + + @override + String get name => switch (inner) { + AFRolePB.Guest => LocaleKeys.settings_appearance_members_guest.tr(), + AFRolePB.Member => LocaleKeys.settings_appearance_members_member.tr(), + AFRolePB.Owner => LocaleKeys.settings_appearance_members_owner.tr(), + _ => throw UnimplementedError('Unknown role: $inner'), + }; + + String get tooltip => switch (inner) { + AFRolePB.Guest => + LocaleKeys.settings_appearance_members_guestHintText.tr(), + AFRolePB.Member => + LocaleKeys.settings_appearance_members_memberHintText.tr(), + AFRolePB.Owner => '', + _ => throw UnimplementedError('Unknown role: $inner'), + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 8976b845d0e7..ef39b1445375 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_members_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; @@ -72,6 +74,10 @@ class SettingsDialog extends StatelessWidget { ); case SettingsPage.workspace: return SettingsWorkspaceView(userProfile: user); + case SettingsPage.member: + return SettingsMembersView(userProfile: user); + case SettingsPage.manageData: + return SettingsManageDataView(userProfile: user); // case SettingsPage.language: // return const SettingsLanguageView(); // case SettingsPage.files: @@ -91,8 +97,6 @@ class SettingsDialog extends StatelessWidget { // ); // case SettingsPage.shortcuts: // return const SettingsCustomizeShortcutsWrapper(); - // case SettingsPage.member: - // return WorkspaceMembersPage(userProfile: user); case SettingsPage.featureFlags: return const FeatureFlagsPage(); default: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index 6b148b8f2b04..e8bedd71da3c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -194,13 +194,7 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent { ) = UpdateWorkspaceMember; } -enum WorkspaceMemberActionType { - none, - get, - add, - remove, - updateRole, -} +enum WorkspaceMemberActionType { none, get, add, remove, updateRole } class WorkspaceMemberActionResult { const WorkspaceMemberActionResult({ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart deleted file mode 100644 index 9779ed763253..000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ /dev/null @@ -1,504 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; - -class WorkspaceMembersPage extends StatelessWidget { - const WorkspaceMembersPage({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => WorkspaceMemberBloc(userProfile: userProfile) - ..add(const WorkspaceMemberEvent.initial()), - child: BlocConsumer( - listener: _showResultDialog, - builder: (context, state) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // title - FlowyText.semibold( - LocaleKeys.settings_appearance_members_title.tr(), - fontSize: 20, - ), - if (state.myRole.canInvite) const _InviteMember(), - if (state.members.isNotEmpty) - _MemberList( - members: state.members, - userProfile: userProfile, - myRole: state.myRole, - ), - const VSpace(48.0), - ], - ), - ); - }, - ), - ); - } - - void _showResultDialog(BuildContext context, WorkspaceMemberState state) { - final actionResult = state.actionResult; - if (actionResult == null) { - return; - } - - final actionType = actionResult.actionType; - final result = actionResult.result; - - // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.add) { - result.fold( - (s) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), - ); - }, - (f) { - final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded - ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() - : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); - showDialog( - context: context, - builder: (context) => NavigatorOkCancelDialog(message: message), - ); - }, - ); - } - - result.onFailure((f) { - Log.error( - '[Member] Failed to perform ${actionType.toString()} action: $f', - ); - }); - } -} - -class _InviteMember extends StatefulWidget { - const _InviteMember(); - - @override - State<_InviteMember> createState() => _InviteMemberState(); -} - -class _InviteMemberState extends State<_InviteMember> { - final _emailController = TextEditingController(); - - @override - void dispose() { - _emailController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(12.0), - FlowyText.semibold( - LocaleKeys.settings_appearance_members_inviteMembers.tr(), - fontSize: 16.0, - ), - const VSpace(8.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 48.0, - ), - child: FlowyTextField( - controller: _emailController, - onEditingComplete: _inviteMember, - ), - ), - ), - const HSpace(10.0), - SizedBox( - height: 48.0, - child: IntrinsicWidth( - child: RoundedTextButton( - title: LocaleKeys.settings_appearance_members_sendInvite.tr(), - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - onPressed: _inviteMember, - ), - ), - ), - ], - ), - const VSpace(16.0), - /* Enable this when the feature is ready - PrimaryButton( - backgroundColor: const Color(0xFFE0E0E0), - child: Padding( - padding: const EdgeInsets.only( - left: 20, - right: 24, - top: 8, - bottom: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.invite_member_link_m, - color: Colors.black, - ), - const HSpace(8.0), - FlowyText( - LocaleKeys.settings_appearance_members_copyInviteLink.tr(), - color: Colors.black, - ), - ], - ), - ), - onPressed: () { - showSnackBarMessage(context, 'not implemented'); - }, - ), - const VSpace(16.0), - */ - const Divider( - height: 1.0, - thickness: 1.0, - ), - ], - ); - } - - void _inviteMember() { - final email = _emailController.text; - if (!isEmail(email)) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_emailInvalidError.tr(), - ); - return; - } - context - .read() - .add(WorkspaceMemberEvent.addWorkspaceMember(email)); - } -} - -class _MemberList extends StatelessWidget { - const _MemberList({ - required this.members, - required this.myRole, - required this.userProfile, - }); - - final List members; - final AFRolePB myRole; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const VSpace(16.0), - SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const Divider(), - children: [ - const _MemberListHeader(), - ...members.map( - (member) => _MemberItem( - member: member, - myRole: myRole, - userProfile: userProfile, - ), - ), - ], - ), - ], - ); - } -} - -class _MemberListHeader extends StatelessWidget { - const _MemberListHeader(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.settings_appearance_members_label.tr(), - fontSize: 16.0, - ), - const VSpace(16.0), - Row( - children: [ - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_user.tr(), - fontSize: 14.0, - ), - ), - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_role.tr(), - fontSize: 14.0, - ), - ), - const HSpace(28.0), - ], - ), - ], - ); - } -} - -class _MemberItem extends StatelessWidget { - const _MemberItem({ - required this.member, - required this.myRole, - required this.userProfile, - }); - - final WorkspaceMemberPB member; - final AFRolePB myRole; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; - return Row( - children: [ - Expanded( - child: FlowyText.medium( - member.name, - color: textColor, - fontSize: 14.0, - ), - ), - Expanded( - child: member.role.isOwner || !myRole.canUpdate - ? FlowyText.medium( - member.role.description, - color: textColor, - fontSize: 14.0, - ) - : _MemberRoleActionList( - member: member, - ), - ), - myRole.canDelete && - member.email != userProfile.email // can't delete self - ? _MemberMoreActionList(member: member) - : const HSpace(28.0), - ], - ); - } -} - -enum _MemberMoreAction { - delete, -} - -class _MemberMoreActionList extends StatelessWidget { - const _MemberMoreActionList({ - required this.member, - }); - - final WorkspaceMemberPB member; - - @override - Widget build(BuildContext context) { - return PopoverActionList<_MemberMoreActionWrapper>( - asBarrier: true, - direction: PopoverDirection.bottomWithCenterAligned, - actions: _MemberMoreAction.values - .map((e) => _MemberMoreActionWrapper(e, member)) - .toList(), - buildChild: (controller) { - return FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, - ), - onTap: () { - controller.show(); - }, - ); - }, - onSelected: (action, controller) { - switch (action.inner) { - case _MemberMoreAction.delete: - showDialog( - context: context, - builder: (_) => NavigatorOkCancelDialog( - title: LocaleKeys.settings_appearance_members_removeMember.tr(), - message: LocaleKeys - .settings_appearance_members_areYouSureToRemoveMember - .tr(), - onOkPressed: () => context.read().add( - WorkspaceMemberEvent.removeWorkspaceMember( - action.member.email, - ), - ), - okTitle: LocaleKeys.button_yes.tr(), - ), - ); - break; - } - controller.close(); - }, - ); - } -} - -class _MemberMoreActionWrapper extends ActionCell { - _MemberMoreActionWrapper(this.inner, this.member); - - final _MemberMoreAction inner; - final WorkspaceMemberPB member; - - @override - String get name { - switch (inner) { - case _MemberMoreAction.delete: - return LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(); - } - } -} - -class _MemberRoleActionList extends StatelessWidget { - const _MemberRoleActionList({ - required this.member, - }); - - final WorkspaceMemberPB member; - - @override - Widget build(BuildContext context) { - return PopoverActionList<_MemberRoleActionWrapper>( - asBarrier: true, - direction: PopoverDirection.bottomWithLeftAligned, - actions: [AFRolePB.Member] - .map((e) => _MemberRoleActionWrapper(e, member)) - .toList(), - offset: const Offset(0, 10), - buildChild: (controller) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => controller.show(), - child: Row( - children: [ - FlowyText.medium( - member.role.description, - fontSize: 14.0, - ), - const HSpace(8.0), - const FlowySvg( - FlowySvgs.drop_menu_show_s, - ), - ], - ), - ), - ); - }, - onSelected: (action, controller) async { - switch (action.inner) { - case AFRolePB.Member: - case AFRolePB.Guest: - context.read().add( - WorkspaceMemberEvent.updateWorkspaceMember( - action.member.email, - action.inner, - ), - ); - break; - case AFRolePB.Owner: - break; - } - controller.close(); - }, - ); - } -} - -class _MemberRoleActionWrapper extends ActionCell { - _MemberRoleActionWrapper(this.inner, this.member); - - final AFRolePB inner; - final WorkspaceMemberPB member; - - @override - Widget? rightIcon(Color iconColor) { - return SizedBox( - width: 58.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyTooltip( - message: tooltip, - child: const FlowySvg( - FlowySvgs.information_s, - // color: iconColor, - ), - ), - const Spacer(), - if (member.role == inner) - const FlowySvg( - FlowySvgs.checkmark_tiny_s, - ), - ], - ), - ); - } - - @override - String get name { - switch (inner) { - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guest.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_member.tr(); - case AFRolePB.Owner: - return LocaleKeys.settings_appearance_members_owner.tr(); - } - throw UnimplementedError('Unknown role: $inner'); - } - - String get tooltip { - switch (inner) { - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guestHintText.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_memberHintText.tr(); - case AFRolePB.Owner: - return ''; - } - throw UnimplementedError('Unknown role: $inner'); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 2abaf51d79eb..1035593bfe7d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -59,6 +60,21 @@ class SettingsMenu extends StatelessWidget { icon: FlowySvgs.settings_workplace_m, changeSelectedPage: changeSelectedPage, ), + if (FeatureFlag.membersSettings.isOn) + SettingsMenuElement( + page: SettingsPage.member, + selectedPage: currentPage, + label: LocaleKeys.settings_appearance_members_label.tr(), + icon: FlowySvgs.settings_members_m, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.manageData, + selectedPage: currentPage, + label: LocaleKeys.settings_manageData_menuLabel.tr(), + icon: FlowySvgs.settings_data_m, + changeSelectedPage: changeSelectedPage, + ), // SettingsMenuElement( // page: SettingsPage.appearance, // selectedPage: currentPage, @@ -108,14 +124,6 @@ class SettingsMenu extends StatelessWidget { // icon: Icons.cut, // changeSelectedPage: changeSelectedPage, // ), - // if (FeatureFlag.membersSettings.isOn) - // SettingsMenuElement( - // page: SettingsPage.member, - // selectedPage: currentPage, - // label: LocaleKeys.settings_appearance_members_label.tr(), - // icon: Icons.people, - // changeSelectedPage: changeSelectedPage, - // ), if (kDebugMode) SettingsMenuElement( // no need to translate this page diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5136d7df5c0f..dddbc52be28e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -390,6 +390,39 @@ "deleteWorkspace": "Delete workspace" } }, + "members": { + "title": "Members Settings", + "inviteMembers": "Invite members", + "sendInvite": "Send invite", + "copyInviteLink": "Copy Invite Link", + "label": "Members", + "user": "User", + "role": "Role", + "removeFromWorkspace": "Remove from Workspace", + "owner": "Owner", + "guest": "Guest", + "member": "Member", + "memberHintText": "A member can read, comment, and edit pages. Invite members and guests.", + "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", + "emailInvalidError": "Invalid email, please check and try again", + "emailSent": "Email sent, please check the inbox", + "members": "members", + "membersCount": { + "zero": "{} members", + "one": "{} member", + "other": "{} members" + }, + "memberLimitExceeded": "You've reached the maximum member limit allowed for your account. If you want to add more additional members to continue your work, please request on Github", + "failedToAddMember": "Failed to add member", + "addMemberSuccess": "Member added successfully", + "removeMember": "Remove Member", + "areYouSureToRemoveMember": "Are you sure you want to remove this member?" + }, + "manageData": { + "menuLabel": "Manage data", + "title": "Manage data", + "description": "Manage data local storage or Import your existing data into Appflowy. You can secure your data with end to end encryption." + }, "common": { "reset": "Reset" },