diff --git a/frontend/appflowy_flutter/integration_test/settings/user_icon_test.dart b/frontend/appflowy_flutter/integration_test/settings/user_icon_test.dart index 492e97f28a..6ea0450cc1 100644 --- a/frontend/appflowy_flutter/integration_test/settings/user_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/settings/user_icon_test.dart @@ -3,6 +3,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + +import '../util/emoji.dart'; import '../util/util.dart'; void main() { @@ -28,27 +30,12 @@ void main() { await tester.pumpAndSettle(); // Select first option that isn't default - await tester.tap(find.byType(IconOption).first); + await tester.tapEmoji('😁'); await tester.pumpAndSettle(); - UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar; - expect(userAvatar.iconUrl, isNotEmpty); - - // Open icon picker dialog again - await tester.tap(userAvatarFinder); - await tester.pumpAndSettle(); - - // Tap the default option - await tester.tap( - find.descendant( - of: find.byType(IconGallery), - matching: find.byType(UserAvatar), - ), - ); - await tester.pumpAndSettle(); - - userAvatar = tester.widget(userAvatarFinder) as UserAvatar; - expect(userAvatar.iconUrl, isEmpty); + final UserAvatar userAvatar = + tester.widget(userAvatarFinder) as UserAvatar; + expect(userAvatar.iconUrl, '😁'); }); }); } 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 df2ee39f24..a6a9e1aa06 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,10 +1,13 @@ 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'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.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'; @@ -31,33 +34,7 @@ class MobileHomePageHeader extends StatelessWidget { constraints: const BoxConstraints(minHeight: 48), child: Row( children: [ - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - // replace with user icon - userIcon.isNotEmpty ? userIcon : '🐻', - fontSize: 26, - ), - onTap: () async { - final icon = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.pageTitle: 'User icon', - }, - ).toString(), - ); - if (icon != null) { - if (context.mounted) { - context.read().add( - SettingsUserEvent.updateUserIcon( - iconUrl: icon.emoji, - ), - ); - } - } - }, - ), + _UserIcon(userIcon: userIcon), const HSpace(12), Expanded( child: Column( @@ -96,3 +73,49 @@ class MobileHomePageHeader extends StatelessWidget { ); } } + +class _UserIcon extends StatelessWidget { + const _UserIcon({ + required this.userIcon, + }); + + final String userIcon; + + @override + Widget build(BuildContext context) { + return FlowyButton( + useIntrinsicWidth: true, + text: builtInSVGIcons.contains(userIcon) + // to be compatible with old user icon + ? FlowySvg( + FlowySvgData('emoji/$userIcon'), + size: const Size.square(32), + blendMode: null, + ) + : FlowyText( + userIcon.isNotEmpty ? userIcon : '🐻', + fontSize: 26, + ), + onTap: () async { + final icon = await context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.pageTitle: + LocaleKeys.titleBar_userIcon.tr(), + }, + ).toString(), + ); + if (icon != null) { + if (context.mounted) { + context.read().add( + SettingsUserEvent.updateUserIcon( + iconUrl: icon.emoji, + ), + ); + } + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart index 380c5fc32b..d0e165cdf3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart @@ -13,7 +13,7 @@ class AppFlowyCloudPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(LocaleKeys.settings_menu_cloudSetting.tr()), + title: Text(LocaleKeys.settings_menu_cloudSettings.tr()), ), body: Padding( padding: const EdgeInsets.all(20.0), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart index dee024ae21..7410554632 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart @@ -17,7 +17,7 @@ class CloudSettingGroup extends StatelessWidget { return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snapshot) => MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_cloudSetting.tr(), + groupTitle: LocaleKeys.settings_menu_cloudSettings.tr(), settingItemList: [ MobileSettingItem( name: LocaleKeys.settings_menu_cloudAppFlowy.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index 73ee188f66..c3dcbe219c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -2,8 +2,9 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -43,9 +44,17 @@ class _RowDetailPageState extends State { @override Widget build(BuildContext context) { return FlowyDialog( - child: BlocProvider( - create: (context) => RowDetailBloc(rowController: widget.rowController) - ..add(const RowDetailEvent.initial()), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + RowDetailBloc(rowController: widget.rowController) + ..add(const RowDetailEvent.initial()), + ), + BlocProvider.value( + value: getIt(), + ), + ], child: ListView( controller: scrollController, children: [ diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index 42fe578c4b..4269b5a7ab 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -189,35 +189,30 @@ class _MobileSignInButton extends StatelessWidget { width: 0.5, ), ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - // The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space. - width: 30, - height: 30, - child: Center( - child: SizedBox( - width: 24, - child: FlowySvg( - icon, - blendMode: null, - ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + // The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space. + width: 30, + height: 30, + child: Center( + child: SizedBox( + width: 24, + child: FlowySvg( + icon, + blendMode: null, ), ), ), - const HSpace(8), - SizedBox( - // To fit the longest label 'Log in with Discord' - width: 135, - child: Text( - labelText, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - ], - ), + ), + const HSpace(8), + Text( + labelText, + style: Theme.of(context).textTheme.titleSmall, + ), + ], ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index 2d0a20ffdd..b8c76f34c1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -1,21 +1,21 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:easy_localization/easy_localization.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/spacing.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; class SidebarUser extends StatelessWidget { const SidebarUser({ @@ -42,7 +42,7 @@ class SidebarUser extends StatelessWidget { iconUrl: state.userProfile.iconUrl, name: state.userProfile.name, ), - const HSpace(10), + const HSpace(4), Expanded( child: _buildUserName(context, state), ), 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 1113bf1b7e..be3c73e81d 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 @@ -61,7 +61,7 @@ class SettingsMenu extends StatelessWidget { SettingsMenuElement( page: SettingsPage.cloud, selectedPage: currentPage, - label: LocaleKeys.settings_menu_cloudSetting.tr(), + label: LocaleKeys.settings_menu_cloudSettings.tr(), icon: Icons.sync, changeSelectedPage: changeSelectedPage, ), 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 803377f262..0415d89a1d 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,9 +1,9 @@ import 'dart:async'; -import 'dart:convert'; 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/util/debounce.dart'; @@ -114,21 +114,16 @@ class SettingsUserView extends StatelessWidget { fontSize: FontSizes.s16, ), children: [ - SizedBox( - height: 300, - width: 300, - child: IconGallery( - defaultOption: _defaultIconOption(context), - selectedIcon: user.iconUrl, - onSelectIcon: (iconUrl, isSelected) { - if (isSelected) { - return Navigator.of(context).pop(); - } - + Container( + height: 380, + width: 360, + margin: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { context .read() - .add(SettingsUserEvent.updateUserIcon(iconUrl: iconUrl)); - Navigator.of(context).pop(); + .add(SettingsUserEvent.updateUserIcon(iconUrl: emoji)); + Navigator.of(dialogContext).pop(); }, ), ), @@ -137,50 +132,6 @@ class SettingsUserView extends StatelessWidget { ); } - // Returns a Widget that is the Default Option for the - // Icon Gallery, enabling users to choose the auto-generated - // icon again. - Widget _defaultIconOption(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - context - .read() - .add(const SettingsUserEvent.removeUserIcon()); - Navigator.of(context).pop(); - }, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: user.iconUrl.isEmpty - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - child: FlowyHover( - style: HoverStyle( - hoverColor: user.iconUrl.isEmpty - ? Colors.transparent - : Theme.of(context).colorScheme.tertiaryContainer, - ), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: DecoratedBox( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: UserAvatar( - iconUrl: "", - name: user.name, - isLarge: true, - ), - ), - ), - ), - ), - ); - } - /// Renders either a login or logout button based on the user's authentication status, or nothing if Supabase is not enabled. /// /// This function checks the current user's authentication type and Supabase @@ -480,6 +431,21 @@ class _AIAccessKeyInputState extends State<_AIAccessKeyInput> { typedef SelectIconCallback = void Function(String iconUrl, bool isSelected); +final builtInSVGIcons = [ + '1F9CC', + '1F9DB', + '1F9DD-200D-2642-FE0F', + '1F9DE-200D-2642-FE0F', + '1F9DF', + '1F42F', + '1F43A', + '1F431', + '1F435', + '1F600', + '1F984', +]; + +// REMOVE this widget in next version 0.3.10 class IconGallery extends StatelessWidget { final String selectedIcon; final SelectIconCallback onSelectIcon; @@ -492,52 +458,26 @@ class IconGallery extends StatelessWidget { this.defaultOption, }); - Future> _getIcons(BuildContext context) async { - final manifestContent = - await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); - - final Map manifestMap = json.decode(manifestContent); - - final iconUrls = manifestMap.keys - .where( - (String key) => - key.startsWith('assets/images/emoji/') && key.endsWith('.svg'), - ) - .map((String key) => key.split('/').last.split('.').first) - .toList(); - - return iconUrls; - } - @override Widget build(BuildContext context) { - return FutureBuilder>( - future: _getIcons(context), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return GridView.count( - padding: const EdgeInsets.all(20), - crossAxisCount: 5, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - children: [ - if (defaultOption != null) defaultOption!, - ...snapshot.data! - .mapIndexed( - (int index, String iconUrl) => IconOption( - emoji: FlowySvgData('emoji/$iconUrl'), - iconUrl: iconUrl, - onSelectIcon: onSelectIcon, - isSelected: iconUrl == selectedIcon, - ), - ) - .toList(), - ], - ); - } - - return const Center(child: CircularProgressIndicator()); - }, + 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, + ), + ) + .toList(), + ], ); } } 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 c8882397ed..31c84332cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,6 +1,8 @@ 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'; import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -66,10 +68,12 @@ class UserAvatar extends StatelessWidget { borderRadius: Corners.s5Border, child: CircleAvatar( backgroundColor: Colors.transparent, - child: FlowySvg( - FlowySvgData('emoji/$iconUrl'), - blendMode: null, - ), + child: builtInSVGIcons.contains(iconUrl) + ? FlowySvg( + FlowySvgData('emoji/$iconUrl'), + blendMode: null, + ) + : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), ), ), ); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index efc2b9fc27..c675e57278 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -268,7 +268,7 @@ "logoutPrompt": "Are you sure to logout?", "selfEncryptionLogoutPrompt": "Are you sure you want to log out? Please ensure you have copied the encryption secret", "syncSetting": "Sync Setting", - "cloudSetting": "Cloud Setting", + "cloudSettings": "Cloud Settings", "enableSync": "Enable sync", "enableEncrypt": "Encrypt data", "cloudURL": "Base URL", @@ -294,7 +294,7 @@ "inputEncryptPrompt": "Please enter your encryption secret for", "clickToCopySecret": "Click to copy secret", "configServerSetting": "Configurate your server settings", - "configServerGuide": "After selecting `Quick Start`, navigate to `Settings` and then \"Cloud Setting\" to configure your self-hosted server.", + "configServerGuide": "After selecting `Quick Start`, navigate to `Settings` and then \"Cloud Settings\" to configure your self-hosted server.", "inputTextFieldHint": "Your secret", "historicalUserList": "User login history", "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button", @@ -1148,6 +1148,7 @@ "font": "Font", "actions": "Actions", "date": "Date", - "addField": "Add field" + "addField": "Add field", + "userIcon": "User icon" } } \ No newline at end of file