From e4eff7e63241d17dfaa83c665f0636e961104eac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 5 Jun 2024 09:18:43 +0800 Subject: [PATCH] feat: support slide actions on mobile (#5463) * feat: support slide actions on mobile * fix: some ui issues * fix: scale down emoji size on windows * fix: flutter analyze * fix: force text height on macos --- frontend/appflowy_flutter/ios/Podfile.lock | 4 +- .../bottom_sheet_add_new_page.dart | 5 + .../bottom_sheet_view_item_body.dart | 14 +- .../default_mobile_action_pane.dart | 157 ++++++++++++++---- .../mobile_home_favorite_folder.dart | 16 +- .../presentation/home/mobile_folders.dart | 2 +- .../home/mobile_home_page_header.dart | 38 ++--- .../home/recent_folder/recent_space.dart | 55 +++--- .../mobile_home_section_folder.dart | 15 ++ .../mobile_home_section_folder_header.dart | 2 +- .../home/shared/mobile_view_card.dart | 41 +++-- .../workspace_menu_bottom_sheet.dart | 40 ++++- .../page_item/mobile_slide_action_button.dart | 4 + .../page_item/mobile_view_item.dart | 96 ----------- .../sidebar/favorites/favorite_folder.dart | 6 +- .../menu/sidebar/header/sidebar_top_menu.dart | 2 +- .../home/menu/sidebar/sidebar.dart | 25 ++- .../workspace/_sidebar_workspace_icon.dart | 6 +- .../sidebar/workspace/sidebar_workspace.dart | 2 +- .../home/menu/view/view_item.dart | 3 - .../flowy_infra_ui/lib/style_widget/text.dart | 24 ++- .../lib/widget/flowy_tooltip.dart | 1 + 22 files changed, 323 insertions(+), 235 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 6f75b60ade..5a2d069c36 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -200,7 +200,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 @@ -227,4 +227,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart index 9bd6f22880..bc8706f8b8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart @@ -21,6 +21,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { children: [ FlowyOptionTile.text( text: LocaleKeys.document_menuName.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.document_s, size: Size.square(18), @@ -30,6 +31,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { ), FlowyOptionTile.text( text: LocaleKeys.grid_menuName.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.grid_s, size: Size.square(18), @@ -39,6 +41,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { ), FlowyOptionTile.text( text: LocaleKeys.board_menuName.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.board_s, size: Size.square(18), @@ -48,6 +51,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { ), FlowyOptionTile.text( text: LocaleKeys.calendar_menuName.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.calendar_s, size: Size.square(18), @@ -57,6 +61,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { ), FlowyOptionTile.text( text: LocaleKeys.chat_newChat.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.chat_ai_page_s, size: Size.square(18), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index d9c6382288..a078521aec 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -44,6 +44,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { case MobileViewItemBottomSheetBodyAction.rename: return FlowyOptionTile.text( text: LocaleKeys.button_rename.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.view_item_rename_s, size: Size.square(18), @@ -57,6 +58,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { case MobileViewItemBottomSheetBodyAction.duplicate: return FlowyOptionTile.text( text: LocaleKeys.button_duplicate.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.duplicate_s, size: Size.square(18), @@ -71,6 +73,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { case MobileViewItemBottomSheetBodyAction.share: return FlowyOptionTile.text( text: LocaleKeys.button_share.tr(), + height: 52.0, leftIcon: const FlowySvg( FlowySvgs.share_s, size: Size.square(18), @@ -84,9 +87,10 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { case MobileViewItemBottomSheetBodyAction.delete: return FlowyOptionTile.text( text: LocaleKeys.button_delete.tr(), + height: 52.0, textColor: Theme.of(context).colorScheme.error, leftIcon: FlowySvg( - FlowySvgs.delete_s, + FlowySvgs.trash_s, size: const Size.square(18), color: Theme.of(context).colorScheme.error, ), @@ -98,6 +102,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ); case MobileViewItemBottomSheetBodyAction.addToFavorites: return FlowyOptionTile.text( + height: 52.0, text: LocaleKeys.button_addToFavorites.tr(), leftIcon: const FlowySvg( FlowySvgs.favorite_s, @@ -111,6 +116,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ); case MobileViewItemBottomSheetBodyAction.removeFromFavorites: return FlowyOptionTile.text( + height: 52.0, text: LocaleKeys.button_removeFromFavorites.tr(), leftIcon: const FlowySvg( FlowySvgs.favorite_section_remove_from_favorite_s, @@ -124,6 +130,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ); case MobileViewItemBottomSheetBodyAction.removeFromRecent: return FlowyOptionTile.text( + height: 52.0, text: LocaleKeys.button_removeFromRecent.tr(), leftIcon: const FlowySvg( FlowySvgs.remove_from_recent_s, @@ -137,7 +144,10 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ); case MobileViewItemBottomSheetBodyAction.divider: - return const Divider(height: 0.5); + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Divider(height: 0.5), + ); } } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index 17ca604717..022d0c224c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -1,10 +1,15 @@ 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/home/shared/mobile_view_card.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flowy_infra/theme_extension.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:flutter_slidable/flutter_slidable.dart'; @@ -13,11 +18,14 @@ enum MobilePaneActionType { delete, addToFavorites, removeFromFavorites, - more; + more, + add; MobileSlideActionButton actionButton( - BuildContext context, - ) { + BuildContext context, { + MobileViewCardType? cardType, + FolderSpaceType? spaceType, + }) { switch (this) { case MobilePaneActionType.delete: return MobileSlideActionButton( @@ -29,59 +37,88 @@ enum MobilePaneActionType { ); case MobilePaneActionType.removeFromFavorites: return MobileSlideActionButton( - backgroundColor: Colors.orange, - svg: FlowySvgs.favorite_s, + backgroundColor: const Color(0xFFFA217F), + svg: FlowySvgs.favorite_section_remove_from_favorite_s, + size: 24.0, onPressed: (context) => context .read() .add(FavoriteEvent.toggle(context.read().view)), ); case MobilePaneActionType.addToFavorites: return MobileSlideActionButton( - backgroundColor: Colors.orange, - svg: FlowySvgs.m_favorite_unselected_lg, - size: 34.0, + backgroundColor: const Color(0xFF00C8FF), + svg: FlowySvgs.favorite_s, + size: 24.0, onPressed: (context) => context .read() .add(FavoriteEvent.toggle(context.read().view)), ); - case MobilePaneActionType.more: + case MobilePaneActionType.add: return MobileSlideActionButton( - backgroundColor: Colors.grey, - svg: FlowySvgs.three_dots_vertical_s, + backgroundColor: const Color(0xFF00C8FF), + svg: FlowySvgs.add_m, size: 28.0, + onPressed: (context) { + final viewBloc = context.read(); + final view = viewBloc.state.view; + final title = view.name; + showMobileBottomSheet( + context, + showHeader: true, + title: title, + showDragHandle: true, + showCloseButton: true, + useRootNavigator: true, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (sheetContext) { + return AddNewPageWidgetBottomSheet( + view: view, + onAction: (layout) { + context.read().add( + ViewEvent.createView( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layout, + section: spaceType!.toViewSectionPB, + ), + ); + }, + ); + }, + ); + }, + ); + case MobilePaneActionType.more: + return MobileSlideActionButton( + backgroundColor: const Color(0xE5515563), + svg: FlowySvgs.three_dots_s, + size: 24.0, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), onPressed: (context) { final viewBloc = context.read(); final favoriteBloc = context.read(); + final recentViewsBloc = context.read(); showMobileBottomSheet( context, showDragHandle: true, showDivider: false, - backgroundColor: AFThemeExtension.of(context).background, useRootNavigator: true, + backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) { return MultiBlocProvider( providers: [ BlocProvider.value(value: viewBloc), BlocProvider.value(value: favoriteBloc), + if (recentViewsBloc != null) + BlocProvider.value(value: recentViewsBloc), ], child: BlocBuilder( builder: (context, state) { - final isFavorite = state.view.isFavorite; return MobileViewItemBottomSheet( view: viewBloc.state.view, - actions: [ - isFavorite - ? MobileViewItemBottomSheetBodyAction - .removeFromFavorites - : MobileViewItemBottomSheetBodyAction - .addToFavorites, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.rename, - if (state.view.layout != ViewLayoutPB.Chat) - MobileViewItemBottomSheetBodyAction.duplicate, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.delete, - ], + actions: _buildActions(state.view, cardType: cardType), ); }, ), @@ -92,19 +129,71 @@ enum MobilePaneActionType { ); } } + + List _buildActions( + ViewPB view, { + MobileViewCardType? cardType, + }) { + final isFavorite = view.isFavorite; + + if (cardType != null) { + switch (cardType) { + case MobileViewCardType.recent: + return [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + if (view.layout != ViewLayoutPB.Chat) + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.removeFromRecent, + ]; + case MobileViewCardType.favorite: + return [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + ]; + } + } + + return [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.rename, + if (view.layout != ViewLayoutPB.Chat) + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.delete, + ]; + } } ActionPane buildEndActionPane( BuildContext context, - List actions, -) { + List actions, { + bool needSpace = true, + MobileViewCardType? cardType, + FolderSpaceType? spaceType, +}) { + debugPrint('actions: $actions'); return ActionPane( motion: const ScrollMotion(), extentRatio: actions.length / 5, - children: actions - .map( - (action) => action.actionButton(context), - ) - .toList(), + children: [ + if (needSpace) const HSpace(20), + ...actions.map( + (action) => action.actionButton( + context, + spaceType: spaceType, + cardType: cardType, + ), + ), + ], ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart index d683cf3507..57ac43f255 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart @@ -63,12 +63,16 @@ class MobileFavoriteFolder extends StatelessWidget { view: view, level: 0, onSelected: context.pushView, - endActionPane: (context) => buildEndActionPane(context, [ - view.isFavorite - ? MobilePaneActionType.removeFromFavorites - : MobilePaneActionType.addToFavorites, - MobilePaneActionType.more, - ]), + endActionPane: (context) => buildEndActionPane( + context, + [ + view.isFavorite + ? MobilePaneActionType.removeFromFavorites + : MobilePaneActionType.addToFavorites, + MobilePaneActionType.more, + ], + spaceType: FolderSpaceType.favorite, + ), ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index 100649701d..78be82ec99 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -111,7 +111,7 @@ class _TrashButton extends StatelessWidget { height: 52, child: FlowyButton( expand: true, - margin: const EdgeInsets.symmetric(vertical: 8), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0), leftIcon: const FlowySvg( FlowySvgs.m_delete_s, ), 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 5ae2ff1430..28d0915aef 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 @@ -123,12 +123,13 @@ class _MobileWorkspace extends StatelessWidget { child: Row( children: [ SizedBox.square( - dimension: 34.0, + dimension: currentWorkspace.icon.isNotEmpty ? 34.0 : 26.0, child: WorkspaceIcon( workspace: currentWorkspace, iconSize: 26, fontSize: 16.0, enableEdit: false, + alignment: Alignment.centerLeft, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( currentWorkspace.workspaceId, @@ -137,32 +138,13 @@ class _MobileWorkspace extends StatelessWidget { ), ), ), - const HSpace(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FlowyText.semibold( - currentWorkspace.name, - fontSize: 16.0, - overflow: TextOverflow.ellipsis, - ), - const HSpace(4.0), - const FlowySvg(FlowySvgs.list_dropdown_s), - ], - ), - FlowyText.regular( - userProfile.email.isNotEmpty - ? userProfile.email - : userProfile.name, - overflow: TextOverflow.ellipsis, - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - ), - ], - ), + currentWorkspace.icon.isNotEmpty + ? const HSpace(2) + : const HSpace(8), + FlowyText.semibold( + currentWorkspace.name, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, ), ], ), @@ -179,7 +161,9 @@ class _MobileWorkspace extends StatelessWidget { showDivider: false, showHeader: true, showDragHandle: true, + showCloseButton: true, title: LocaleKeys.workspace_menuTitle.tr(), + backgroundColor: Theme.of(context).colorScheme.surface, builder: (_) { return BlocProvider.value( value: context.read(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart index 3cab831c23..3523d75720 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart @@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class MobileRecentSpace extends StatefulWidget { const MobileRecentSpace({super.key}); @@ -62,34 +63,36 @@ class _RecentViews extends StatelessWidget { @override Widget build(BuildContext context) { - return Scrollbar( - child: ListView.separated( - key: const PageStorageKey('recent_views_page_storage_key'), - padding: const EdgeInsets.symmetric( - horizontal: HomeSpaceViewSizes.mHorizontalPadding, - ), - itemBuilder: (context, index) { - final sectionView = recentViews[index]; - return Container( - padding: const EdgeInsets.symmetric(vertical: 24.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 0.5, + return SlidableAutoCloseBehavior( + child: Scrollbar( + child: ListView.separated( + key: const PageStorageKey('recent_views_page_storage_key'), + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + ), + itemBuilder: (context, index) { + final sectionView = recentViews[index]; + return Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 0.5, + ), ), ), - ), - child: MobileViewCard( - key: ValueKey(sectionView.item.id), - view: sectionView.item, - timestamp: sectionView.timestamp, - type: MobileViewCardType.recent, - ), - ); - }, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: recentViews.length, + child: MobileViewCard( + key: ValueKey(sectionView.item.id), + view: sectionView.item, + timestamp: sectionView.timestamp, + type: MobileViewCardType.recent, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: recentViews.length, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index 7a14a4d48d..4cd212f5a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,9 +1,11 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -69,6 +71,19 @@ class MobileSectionFolder extends StatelessWidget { leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, onSelected: context.pushView, + endActionPane: (context) { + final view = context.read().state.view; + return buildEndActionPane( + context, + [ + MobilePaneActionType.more, + if (view.layout == ViewLayoutPB.Document) + MobilePaneActionType.add, + ], + spaceType: spaceType, + needSpace: false, + ); + }, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart index 49a9829d8a..aa84dc7601 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart @@ -38,7 +38,7 @@ class _MobileSectionFolderHeaderState extends State { widget.title, fontSize: 16.0, ), - margin: const EdgeInsets.symmetric(vertical: 8), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0), expandText: false, iconPadding: 2, mainAxisAlignment: MainAxisAlignment.start, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart index 78f03569be..b995236279 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart @@ -22,6 +22,7 @@ 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 'package:flutter_slidable/flutter_slidable.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:string_validator/string_validator.dart'; @@ -63,21 +64,33 @@ class MobileViewCard extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTapUp: (_) => context.pushView(view), - onLongPressUp: () => _showActionSheet(context), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded(child: _buildDescription(context, state)), - const HSpace(20.0), - SizedBox( - width: 84, - height: 60, - child: _buildCover(context, state), - ), + return Slidable( + endActionPane: buildEndActionPane( + context, + [ + MobilePaneActionType.more, + context.watch().state.view.isFavorite + ? MobilePaneActionType.removeFromFavorites + : MobilePaneActionType.addToFavorites, ], + cardType: type, + ), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: (_) => context.pushView(view), + onLongPressUp: () => _showActionSheet(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: _buildDescription(context, state)), + const HSpace(20.0), + SizedBox( + width: 84, + height: 60, + child: _buildCover(context, state), + ), + ], + ), ), ); }, 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 496c5607a5..17962bcc3c 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,6 +1,7 @@ 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/util/theme_extension.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'; @@ -27,7 +28,13 @@ class MobileWorkspaceMenu extends StatelessWidget { @override Widget build(BuildContext context) { - final List children = []; + final List children = [ + _WorkspaceUserItem(userProfile: userProfile), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Divider(height: 0.5), + ), + ]; for (var i = 0; i < workspaces.length; i++) { final workspace = workspaces[i]; children.add( @@ -35,7 +42,7 @@ class MobileWorkspaceMenu extends StatelessWidget { key: ValueKey(workspace.workspaceId), userProfile: userProfile, workspace: workspace, - showTopBorder: i == 0, + showTopBorder: false, currentWorkspace: currentWorkspace, onWorkspaceSelected: onWorkspaceSelected, ), @@ -47,6 +54,34 @@ class MobileWorkspaceMenu extends StatelessWidget { } } +class _WorkspaceUserItem extends StatelessWidget { + const _WorkspaceUserItem({required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).isLightMode + ? const Color(0x99333333) + : const Color(0x99CCCCCC); + return FlowyOptionTile.text( + height: 32, + showTopBorder: false, + showBottomBorder: false, + content: Expanded( + child: Padding( + padding: const EdgeInsets.only(), + child: FlowyText( + userProfile.email, + fontSize: 14, + color: color, + ), + ), + ), + ); + } +} + class _WorkspaceMenuItem extends StatelessWidget { const _WorkspaceMenuItem({ super.key, @@ -102,6 +137,7 @@ class _WorkspaceMenuItem extends StatelessWidget { ), height: 60, showTopBorder: showTopBorder, + showBottomBorder: false, leftIcon: WorkspaceIcon( enableEdit: false, iconSize: 26, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart index 5aebb53100..b3d021613e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart @@ -9,6 +9,7 @@ class MobileSlideActionButton extends StatelessWidget { required this.svg, this.size = 32.0, this.backgroundColor = Colors.transparent, + this.borderRadius = BorderRadius.zero, required this.onPressed, }); @@ -16,15 +17,18 @@ class MobileSlideActionButton extends StatelessWidget { final double size; final Color backgroundColor; final SlidableActionCallback onPressed; + final BorderRadius borderRadius; @override Widget build(BuildContext context) { return CustomSlidableAction( + borderRadius: borderRadius, backgroundColor: backgroundColor, onPressed: (context) { HapticFeedback.mediumImpact(); onPressed(context); }, + padding: EdgeInsets.zero, child: FlowySvg( svg, size: Size.square(size), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 4b4672f6a7..2d20581b98 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -1,16 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.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'; @@ -270,17 +265,6 @@ class _SingleMobileInnerViewItemState extends State { ), ]; - // hover action - - // ยทยทยท more action button - children.add(_buildViewMoreButton(context)); - // only support add button for document layout - if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) { - // + button - - children.add(_buildViewAddButton(context)); - } - Widget child = InkWell( borderRadius: BorderRadius.circular(4.0), onTap: () => widget.onSelected(widget.view), @@ -345,86 +329,6 @@ class _SingleMobileInnerViewItemState extends State { }, ); } - - // + button - Widget _buildViewAddButton(BuildContext context) { - return MobileViewAddButton( - onPressed: () { - final title = widget.view.name; - showMobileBottomSheet( - context, - showHeader: true, - title: title, - showDragHandle: true, - showCloseButton: true, - useRootNavigator: true, - builder: (sheetContext) { - return AddNewPageWidgetBottomSheet( - view: widget.view, - onAction: (layout) { - Navigator.of(sheetContext).pop(); - context.read().add( - ViewEvent.createView( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layout, - section: widget.spaceType != FolderSpaceType.favorite - ? widget.spaceType.toViewSectionPB - : null, - ), - ); - }, - ); - }, - ); - }, - ); - } - - // + button - Widget _buildViewMoreButton(BuildContext context) { - return MobileViewMoreButton(onPressed: () => _showMoreActions(context)); - } - - Future _showMoreActions(BuildContext context) async { - final viewBloc = context.read(); - final favoriteBloc = context.read(); - await showMobileBottomSheet( - context, - showHeader: true, - title: widget.view.name, - showDragHandle: true, - showCloseButton: true, - useRootNavigator: true, - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: viewBloc), - BlocProvider.value(value: favoriteBloc), - ], - child: BlocBuilder( - builder: (context, state) { - final isFavorite = state.view.isFavorite; - return MobileViewItemBottomSheet( - view: viewBloc.state.view, - actions: [ - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.rename, - MobileViewItemBottomSheetBodyAction.divider, - if (state.view.layout != ViewLayoutPB.Chat) - MobileViewItemBottomSheetBodyAction.duplicate, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.delete, - ], - ); - }, - ), - ); - }, - ); - } } // workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field. diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart index a3cb50fd06..f73fa04cd0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart @@ -189,11 +189,13 @@ class FavoriteMoreButton extends StatelessWidget { margin: EdgeInsets.zero, child: FlowyButton( onTap: () {}, - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0), + margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 7.0), leftIcon: const FlowySvg( FlowySvgs.workspace_three_dots_s, ), - text: FlowyText.regular(LocaleKeys.button_more.tr()), + text: FlowyText.regular( + LocaleKeys.button_more.tr(), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 240a660966..67bcbc782f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -88,7 +88,7 @@ class SidebarTopMenu extends StatelessWidget { builder: (_, value, ___) => Opacity( opacity: value ? 1 : 0, child: Padding( - padding: const EdgeInsets.only(top: 12.0, right: 4.0), + padding: const EdgeInsets.only(top: 12.0, right: 6.0), child: FlowyTooltip( richMessage: textSpan, child: Listener( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index c7d8e3fbc2..6956bc51d6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -191,6 +191,7 @@ class _SidebarState extends State<_Sidebar> { Timer? _scrollDebounce; bool _isScrolling = false; final _isHovered = ValueNotifier(false); + final _scrollOffset = ValueNotifier(0); @override void initState() { @@ -203,6 +204,7 @@ class _SidebarState extends State<_Sidebar> { _scrollDebounce?.cancel(); _scrollController.removeListener(_onScrollChanged); _scrollController.dispose(); + _scrollOffset.dispose(); _isHovered.dispose(); super.dispose(); } @@ -255,11 +257,20 @@ class _SidebarState extends State<_Sidebar> { const SidebarNewPageButton(), // scrollable document list const VSpace(12.0), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Divider( - color: Color(0x1E1F2329), - height: 0.5, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: ValueListenableBuilder( + valueListenable: _scrollOffset, + builder: (_, offset, child) { + return Opacity( + opacity: offset > 0 ? 1 : 0, + child: child, + ); + }, + child: const Divider( + color: Color(0x141F2329), + height: 0.5, + ), ), ), Expanded( @@ -281,7 +292,7 @@ class _SidebarState extends State<_Sidebar> { Padding( padding: menuHorizontalInset + const EdgeInsets.symmetric(horizontal: 4.0), - child: const Divider(height: 1.0, color: Color(0x141F2329)), + child: const Divider(height: 0.5, color: Color(0x141F2329)), ), const VSpace(8), Padding( @@ -302,6 +313,8 @@ class _SidebarState extends State<_Sidebar> { _scrollDebounce?.cancel(); _scrollDebounce = Timer(const Duration(milliseconds: 300), _setScrollStopped); + + _scrollOffset.value = _scrollController.offset; } void _setScrollStopped() { 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 6f96e6abc2..c05cf1e148 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 @@ -17,6 +17,7 @@ class WorkspaceIcon extends StatefulWidget { required this.onSelected, this.borderRadius = 4, this.emojiSize, + this.alignment, }); final UserWorkspacePB workspace; @@ -26,6 +27,7 @@ class WorkspaceIcon extends StatefulWidget { final double? emojiSize; final void Function(EmojiPickerResult) onSelected; final double borderRadius; + final Alignment? alignment; @override State createState() => _WorkspaceIconState(); @@ -38,8 +40,8 @@ class _WorkspaceIconState extends State { Widget build(BuildContext context) { Widget child = widget.workspace.icon.isNotEmpty ? Container( - width: widget.iconSize, - alignment: Alignment.center, + width: widget.emojiSize ?? widget.iconSize, + alignment: widget.alignment ?? Alignment.center, child: FlowyText.emoji( widget.workspace.icon, fontSize: widget.emojiSize ?? widget.iconSize, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 1dddce8ac6..2fa91a8da1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -50,7 +50,7 @@ class _SidebarWorkspaceState extends State { UserSettingButton(userProfile: widget.userProfile), const HSpace(8.0), const NotificationButton(), - const HSpace(10.0), + const HSpace(12.0), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index cdbbd76e33..6b74be1e4f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -434,7 +434,6 @@ class _SingleInnerViewItemState extends State { child: FlowyText.regular( widget.view.name, overflow: TextOverflow.ellipsis, - lineHeight: 1.1, ), ), ]; @@ -466,7 +465,6 @@ class _SingleInnerViewItemState extends State { child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: children, ), ), @@ -499,7 +497,6 @@ class _SingleInnerViewItemState extends State { ? FlowyText.emoji( widget.view.icon.value, fontSize: 16.0, - lineHeight: 1.4, ) : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index d5d04ddb98..71be43364f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -const String _emojiFontFamily = 'noto color emoji'; - class FlowyText extends StatelessWidget { final String text; final TextOverflow? overflow; @@ -138,16 +136,13 @@ class FlowyText extends StatelessWidget { var fontFamily = this.fontFamily; var fallbackFontFamily = this.fallbackFontFamily; - if (isEmoji && (Platform.isLinux || Platform.isAndroid)) { + var fontSize = + this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!; + if (isEmoji && _useNotoColorEmoji) { fontFamily = _loadEmojiFontFamilyIfNeeded(); if (fontFamily != null && fallbackFontFamily == null) { fallbackFontFamily = [fontFamily]; } - } - - var fontSize = - this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!; - if (Platform.isLinux && fontFamily == _emojiFontFamily) { fontSize = fontSize * 0.8; } @@ -175,6 +170,14 @@ class FlowyText extends StatelessWidget { textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, style: textStyle, + strutStyle: Platform.isMacOS + ? StrutStyle.fromTextStyle( + textStyle, + forceStrutHeight: true, + leadingDistribution: TextLeadingDistribution.even, + height: 1.1, + ) + : null, ); } @@ -189,10 +192,13 @@ class FlowyText extends StatelessWidget { } String? _loadEmojiFontFamilyIfNeeded() { - if (Platform.isLinux || Platform.isAndroid) { + if (_useNotoColorEmoji) { return GoogleFonts.notoColorEmoji().fontFamily; } return null; } + + bool get _useNotoColorEmoji => + Platform.isLinux || Platform.isAndroid || Platform.isWindows; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index 382b18fc0e..a50d500348 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -25,6 +25,7 @@ class FlowyTooltip extends StatelessWidget { final isLightMode = Theme.of(context).brightness == Brightness.light; return Tooltip( margin: margin, + verticalOffset: 16.0, padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( color: isLightMode ? const Color(0xE5171717) : const Color(0xE5E5E5E5),