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
This commit is contained in:
Lucas.Xu 2024-06-05 09:18:43 +08:00 committed by GitHub
parent 9f66dcdc8f
commit e4eff7e632
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 323 additions and 235 deletions

View File

@ -200,7 +200,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -227,4 +227,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.11.3 COCOAPODS: 1.15.2

View File

@ -21,6 +21,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
children: [ children: [
FlowyOptionTile.text( FlowyOptionTile.text(
text: LocaleKeys.document_menuName.tr(), text: LocaleKeys.document_menuName.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.document_s, FlowySvgs.document_s,
size: Size.square(18), size: Size.square(18),
@ -30,6 +31,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
text: LocaleKeys.grid_menuName.tr(), text: LocaleKeys.grid_menuName.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.grid_s, FlowySvgs.grid_s,
size: Size.square(18), size: Size.square(18),
@ -39,6 +41,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
text: LocaleKeys.board_menuName.tr(), text: LocaleKeys.board_menuName.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.board_s, FlowySvgs.board_s,
size: Size.square(18), size: Size.square(18),
@ -48,6 +51,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
text: LocaleKeys.calendar_menuName.tr(), text: LocaleKeys.calendar_menuName.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.calendar_s, FlowySvgs.calendar_s,
size: Size.square(18), size: Size.square(18),
@ -57,6 +61,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
text: LocaleKeys.chat_newChat.tr(), text: LocaleKeys.chat_newChat.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.chat_ai_page_s, FlowySvgs.chat_ai_page_s,
size: Size.square(18), size: Size.square(18),

View File

@ -44,6 +44,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
case MobileViewItemBottomSheetBodyAction.rename: case MobileViewItemBottomSheetBodyAction.rename:
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: LocaleKeys.button_rename.tr(), text: LocaleKeys.button_rename.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.view_item_rename_s, FlowySvgs.view_item_rename_s,
size: Size.square(18), size: Size.square(18),
@ -57,6 +58,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
case MobileViewItemBottomSheetBodyAction.duplicate: case MobileViewItemBottomSheetBodyAction.duplicate:
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: LocaleKeys.button_duplicate.tr(), text: LocaleKeys.button_duplicate.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.duplicate_s, FlowySvgs.duplicate_s,
size: Size.square(18), size: Size.square(18),
@ -71,6 +73,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
case MobileViewItemBottomSheetBodyAction.share: case MobileViewItemBottomSheetBodyAction.share:
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: LocaleKeys.button_share.tr(), text: LocaleKeys.button_share.tr(),
height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.share_s, FlowySvgs.share_s,
size: Size.square(18), size: Size.square(18),
@ -84,9 +87,10 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
case MobileViewItemBottomSheetBodyAction.delete: case MobileViewItemBottomSheetBodyAction.delete:
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: LocaleKeys.button_delete.tr(), text: LocaleKeys.button_delete.tr(),
height: 52.0,
textColor: Theme.of(context).colorScheme.error, textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg( leftIcon: FlowySvg(
FlowySvgs.delete_s, FlowySvgs.trash_s,
size: const Size.square(18), size: const Size.square(18),
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
), ),
@ -98,6 +102,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
); );
case MobileViewItemBottomSheetBodyAction.addToFavorites: case MobileViewItemBottomSheetBodyAction.addToFavorites:
return FlowyOptionTile.text( return FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.button_addToFavorites.tr(), text: LocaleKeys.button_addToFavorites.tr(),
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.favorite_s, FlowySvgs.favorite_s,
@ -111,6 +116,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
); );
case MobileViewItemBottomSheetBodyAction.removeFromFavorites: case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
return FlowyOptionTile.text( return FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.button_removeFromFavorites.tr(), text: LocaleKeys.button_removeFromFavorites.tr(),
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.favorite_section_remove_from_favorite_s, FlowySvgs.favorite_section_remove_from_favorite_s,
@ -124,6 +130,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
); );
case MobileViewItemBottomSheetBodyAction.removeFromRecent: case MobileViewItemBottomSheetBodyAction.removeFromRecent:
return FlowyOptionTile.text( return FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.button_removeFromRecent.tr(), text: LocaleKeys.button_removeFromRecent.tr(),
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.remove_from_recent_s, FlowySvgs.remove_from_recent_s,
@ -137,7 +144,10 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
); );
case MobileViewItemBottomSheetBodyAction.divider: case MobileViewItemBottomSheetBodyAction.divider:
return const Divider(height: 0.5); return const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Divider(height: 0.5),
);
} }
} }
} }

View File

@ -1,10 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/mobile/presentation/page_item/mobile_slide_action_button.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.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/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
@ -13,11 +18,14 @@ enum MobilePaneActionType {
delete, delete,
addToFavorites, addToFavorites,
removeFromFavorites, removeFromFavorites,
more; more,
add;
MobileSlideActionButton actionButton( MobileSlideActionButton actionButton(
BuildContext context, BuildContext context, {
) { MobileViewCardType? cardType,
FolderSpaceType? spaceType,
}) {
switch (this) { switch (this) {
case MobilePaneActionType.delete: case MobilePaneActionType.delete:
return MobileSlideActionButton( return MobileSlideActionButton(
@ -29,59 +37,88 @@ enum MobilePaneActionType {
); );
case MobilePaneActionType.removeFromFavorites: case MobilePaneActionType.removeFromFavorites:
return MobileSlideActionButton( return MobileSlideActionButton(
backgroundColor: Colors.orange, backgroundColor: const Color(0xFFFA217F),
svg: FlowySvgs.favorite_s, svg: FlowySvgs.favorite_section_remove_from_favorite_s,
size: 24.0,
onPressed: (context) => context onPressed: (context) => context
.read<FavoriteBloc>() .read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)), .add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
); );
case MobilePaneActionType.addToFavorites: case MobilePaneActionType.addToFavorites:
return MobileSlideActionButton( return MobileSlideActionButton(
backgroundColor: Colors.orange, backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.m_favorite_unselected_lg, svg: FlowySvgs.favorite_s,
size: 34.0, size: 24.0,
onPressed: (context) => context onPressed: (context) => context
.read<FavoriteBloc>() .read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)), .add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
); );
case MobilePaneActionType.more: case MobilePaneActionType.add:
return MobileSlideActionButton( return MobileSlideActionButton(
backgroundColor: Colors.grey, backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.three_dots_vertical_s, svg: FlowySvgs.add_m,
size: 28.0, size: 28.0,
onPressed: (context) {
final viewBloc = context.read<ViewBloc>();
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<ViewBloc>().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) { onPressed: (context) {
final viewBloc = context.read<ViewBloc>(); final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>(); final favoriteBloc = context.read<FavoriteBloc>();
final recentViewsBloc = context.read<RecentViewsBloc?>();
showMobileBottomSheet( showMobileBottomSheet(
context, context,
showDragHandle: true, showDragHandle: true,
showDivider: false, showDivider: false,
backgroundColor: AFThemeExtension.of(context).background,
useRootNavigator: true, useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) { builder: (context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider.value(value: viewBloc), BlocProvider.value(value: viewBloc),
BlocProvider.value(value: favoriteBloc), BlocProvider.value(value: favoriteBloc),
if (recentViewsBloc != null)
BlocProvider.value(value: recentViewsBloc),
], ],
child: BlocBuilder<ViewBloc, ViewState>( child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) { builder: (context, state) {
final isFavorite = state.view.isFavorite;
return MobileViewItemBottomSheet( return MobileViewItemBottomSheet(
view: viewBloc.state.view, view: viewBloc.state.view,
actions: [ actions: _buildActions(state.view, cardType: cardType),
isFavorite
? MobileViewItemBottomSheetBodyAction
.removeFromFavorites
: MobileViewItemBottomSheetBodyAction
.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.rename,
if (state.view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.delete,
],
); );
}, },
), ),
@ -92,19 +129,71 @@ enum MobilePaneActionType {
); );
} }
} }
List<MobileViewItemBottomSheetBodyAction> _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( ActionPane buildEndActionPane(
BuildContext context, BuildContext context,
List<MobilePaneActionType> actions, List<MobilePaneActionType> actions, {
) { bool needSpace = true,
MobileViewCardType? cardType,
FolderSpaceType? spaceType,
}) {
debugPrint('actions: $actions');
return ActionPane( return ActionPane(
motion: const ScrollMotion(), motion: const ScrollMotion(),
extentRatio: actions.length / 5, extentRatio: actions.length / 5,
children: actions children: [
.map( if (needSpace) const HSpace(20),
(action) => action.actionButton(context), ...actions.map(
) (action) => action.actionButton(
.toList(), context,
spaceType: spaceType,
cardType: cardType,
),
),
],
); );
} }

View File

@ -63,12 +63,16 @@ class MobileFavoriteFolder extends StatelessWidget {
view: view, view: view,
level: 0, level: 0,
onSelected: context.pushView, onSelected: context.pushView,
endActionPane: (context) => buildEndActionPane(context, [ endActionPane: (context) => buildEndActionPane(
context,
[
view.isFavorite view.isFavorite
? MobilePaneActionType.removeFromFavorites ? MobilePaneActionType.removeFromFavorites
: MobilePaneActionType.addToFavorites, : MobilePaneActionType.addToFavorites,
MobilePaneActionType.more, MobilePaneActionType.more,
]), ],
spaceType: FolderSpaceType.favorite,
),
), ),
), ),
], ],

View File

@ -111,7 +111,7 @@ class _TrashButton extends StatelessWidget {
height: 52, height: 52,
child: FlowyButton( child: FlowyButton(
expand: true, expand: true,
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0),
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.m_delete_s, FlowySvgs.m_delete_s,
), ),

View File

@ -123,12 +123,13 @@ class _MobileWorkspace extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
SizedBox.square( SizedBox.square(
dimension: 34.0, dimension: currentWorkspace.icon.isNotEmpty ? 34.0 : 26.0,
child: WorkspaceIcon( child: WorkspaceIcon(
workspace: currentWorkspace, workspace: currentWorkspace,
iconSize: 26, iconSize: 26,
fontSize: 16.0, fontSize: 16.0,
enableEdit: false, enableEdit: false,
alignment: Alignment.centerLeft,
onSelected: (result) => context.read<UserWorkspaceBloc>().add( onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon( UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId, currentWorkspace.workspaceId,
@ -137,33 +138,14 @@ class _MobileWorkspace extends StatelessWidget {
), ),
), ),
), ),
const HSpace(8), currentWorkspace.icon.isNotEmpty
Expanded( ? const HSpace(2)
child: Column( : const HSpace(8),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FlowyText.semibold( FlowyText.semibold(
currentWorkspace.name, currentWorkspace.name,
fontSize: 16.0, fontSize: 16.0,
overflow: TextOverflow.ellipsis, 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,
),
],
),
),
], ],
), ),
); );
@ -179,7 +161,9 @@ class _MobileWorkspace extends StatelessWidget {
showDivider: false, showDivider: false,
showHeader: true, showHeader: true,
showDragHandle: true, showDragHandle: true,
showCloseButton: true,
title: LocaleKeys.workspace_menuTitle.tr(), title: LocaleKeys.workspace_menuTitle.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) { builder: (_) {
return BlocProvider.value( return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(), value: context.read<UserWorkspaceBloc>(),

View File

@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class MobileRecentSpace extends StatefulWidget { class MobileRecentSpace extends StatefulWidget {
const MobileRecentSpace({super.key}); const MobileRecentSpace({super.key});
@ -62,7 +63,8 @@ class _RecentViews extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scrollbar( return SlidableAutoCloseBehavior(
child: Scrollbar(
child: ListView.separated( child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'), key: const PageStorageKey('recent_views_page_storage_key'),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -91,6 +93,7 @@ class _RecentViews extends StatelessWidget {
separatorBuilder: (context, index) => const HSpace(8), separatorBuilder: (context, index) => const HSpace(8),
itemCount: recentViews.length, itemCount: recentViews.length,
), ),
),
); );
} }
} }

View File

@ -1,9 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.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/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.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/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_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/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -69,6 +71,19 @@ class MobileSectionFolder extends StatelessWidget {
leftPadding: HomeSpaceViewSizes.leftPadding, leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false, isFeedback: false,
onSelected: context.pushView, onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
spaceType: spaceType,
needSpace: false,
);
},
), ),
), ),
], ],

View File

@ -38,7 +38,7 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
widget.title, widget.title,
fontSize: 16.0, fontSize: 16.0,
), ),
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0),
expandText: false, expandText: false,
iconPadding: 2, iconPadding: 2,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,

View File

@ -22,6 +22,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
@ -63,7 +64,18 @@ class MobileViewCard extends StatelessWidget {
], ],
child: BlocBuilder<RecentViewBloc, RecentViewState>( child: BlocBuilder<RecentViewBloc, RecentViewState>(
builder: (context, state) { builder: (context, state) {
return GestureDetector( return Slidable(
endActionPane: buildEndActionPane(
context,
[
MobilePaneActionType.more,
context.watch<ViewBloc>().state.view.isFavorite
? MobilePaneActionType.removeFromFavorites
: MobilePaneActionType.addToFavorites,
],
cardType: type,
),
child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTapUp: (_) => context.pushView(view), onTapUp: (_) => context.pushView(view),
onLongPressUp: () => _showActionSheet(context), onLongPressUp: () => _showActionSheet(context),
@ -79,6 +91,7 @@ class MobileViewCard extends StatelessWidget {
), ),
], ],
), ),
),
); );
}, },
), ),

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.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/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.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/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
@ -27,7 +28,13 @@ class MobileWorkspaceMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> children = []; final List<Widget> 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++) { for (var i = 0; i < workspaces.length; i++) {
final workspace = workspaces[i]; final workspace = workspaces[i];
children.add( children.add(
@ -35,7 +42,7 @@ class MobileWorkspaceMenu extends StatelessWidget {
key: ValueKey(workspace.workspaceId), key: ValueKey(workspace.workspaceId),
userProfile: userProfile, userProfile: userProfile,
workspace: workspace, workspace: workspace,
showTopBorder: i == 0, showTopBorder: false,
currentWorkspace: currentWorkspace, currentWorkspace: currentWorkspace,
onWorkspaceSelected: onWorkspaceSelected, 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 { class _WorkspaceMenuItem extends StatelessWidget {
const _WorkspaceMenuItem({ const _WorkspaceMenuItem({
super.key, super.key,
@ -102,6 +137,7 @@ class _WorkspaceMenuItem extends StatelessWidget {
), ),
height: 60, height: 60,
showTopBorder: showTopBorder, showTopBorder: showTopBorder,
showBottomBorder: false,
leftIcon: WorkspaceIcon( leftIcon: WorkspaceIcon(
enableEdit: false, enableEdit: false,
iconSize: 26, iconSize: 26,

View File

@ -9,6 +9,7 @@ class MobileSlideActionButton extends StatelessWidget {
required this.svg, required this.svg,
this.size = 32.0, this.size = 32.0,
this.backgroundColor = Colors.transparent, this.backgroundColor = Colors.transparent,
this.borderRadius = BorderRadius.zero,
required this.onPressed, required this.onPressed,
}); });
@ -16,15 +17,18 @@ class MobileSlideActionButton extends StatelessWidget {
final double size; final double size;
final Color backgroundColor; final Color backgroundColor;
final SlidableActionCallback onPressed; final SlidableActionCallback onPressed;
final BorderRadius borderRadius;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomSlidableAction( return CustomSlidableAction(
borderRadius: borderRadius,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
onPressed: (context) { onPressed: (context) {
HapticFeedback.mediumImpact(); HapticFeedback.mediumImpact();
onPressed(context); onPressed(context);
}, },
padding: EdgeInsets.zero,
child: FlowySvg( child: FlowySvg(
svg, svg,
size: Size.square(size), size: Size.square(size),

View File

@ -1,16 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.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/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.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/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -270,17 +265,6 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
), ),
]; ];
// 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( Widget child = InkWell(
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
onTap: () => widget.onSelected(widget.view), onTap: () => widget.onSelected(widget.view),
@ -345,86 +329,6 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
}, },
); );
} }
// + 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<ViewBloc>().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<void> _showMoreActions(BuildContext context) async {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
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<ViewBloc, ViewState>(
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. // 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.

View File

@ -189,11 +189,13 @@ class FavoriteMoreButton extends StatelessWidget {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: FlowyButton( child: FlowyButton(
onTap: () {}, onTap: () {},
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0), margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 7.0),
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.workspace_three_dots_s, FlowySvgs.workspace_three_dots_s,
), ),
text: FlowyText.regular(LocaleKeys.button_more.tr()), text: FlowyText.regular(
LocaleKeys.button_more.tr(),
),
), ),
); );
} }

View File

@ -88,7 +88,7 @@ class SidebarTopMenu extends StatelessWidget {
builder: (_, value, ___) => Opacity( builder: (_, value, ___) => Opacity(
opacity: value ? 1 : 0, opacity: value ? 1 : 0,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 12.0, right: 4.0), padding: const EdgeInsets.only(top: 12.0, right: 6.0),
child: FlowyTooltip( child: FlowyTooltip(
richMessage: textSpan, richMessage: textSpan,
child: Listener( child: Listener(

View File

@ -191,6 +191,7 @@ class _SidebarState extends State<_Sidebar> {
Timer? _scrollDebounce; Timer? _scrollDebounce;
bool _isScrolling = false; bool _isScrolling = false;
final _isHovered = ValueNotifier(false); final _isHovered = ValueNotifier(false);
final _scrollOffset = ValueNotifier<double>(0);
@override @override
void initState() { void initState() {
@ -203,6 +204,7 @@ class _SidebarState extends State<_Sidebar> {
_scrollDebounce?.cancel(); _scrollDebounce?.cancel();
_scrollController.removeListener(_onScrollChanged); _scrollController.removeListener(_onScrollChanged);
_scrollController.dispose(); _scrollController.dispose();
_scrollOffset.dispose();
_isHovered.dispose(); _isHovered.dispose();
super.dispose(); super.dispose();
} }
@ -255,13 +257,22 @@ class _SidebarState extends State<_Sidebar> {
const SidebarNewPageButton(), const SidebarNewPageButton(),
// scrollable document list // scrollable document list
const VSpace(12.0), const VSpace(12.0),
const Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Divider( child: ValueListenableBuilder(
color: Color(0x1E1F2329), valueListenable: _scrollOffset,
builder: (_, offset, child) {
return Opacity(
opacity: offset > 0 ? 1 : 0,
child: child,
);
},
child: const Divider(
color: Color(0x141F2329),
height: 0.5, height: 0.5,
), ),
), ),
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: menuHorizontalInset - const EdgeInsets.only(right: 6), padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
@ -281,7 +292,7 @@ class _SidebarState extends State<_Sidebar> {
Padding( Padding(
padding: menuHorizontalInset + padding: menuHorizontalInset +
const EdgeInsets.symmetric(horizontal: 4.0), 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), const VSpace(8),
Padding( Padding(
@ -302,6 +313,8 @@ class _SidebarState extends State<_Sidebar> {
_scrollDebounce?.cancel(); _scrollDebounce?.cancel();
_scrollDebounce = _scrollDebounce =
Timer(const Duration(milliseconds: 300), _setScrollStopped); Timer(const Duration(milliseconds: 300), _setScrollStopped);
_scrollOffset.value = _scrollController.offset;
} }
void _setScrollStopped() { void _setScrollStopped() {

View File

@ -17,6 +17,7 @@ class WorkspaceIcon extends StatefulWidget {
required this.onSelected, required this.onSelected,
this.borderRadius = 4, this.borderRadius = 4,
this.emojiSize, this.emojiSize,
this.alignment,
}); });
final UserWorkspacePB workspace; final UserWorkspacePB workspace;
@ -26,6 +27,7 @@ class WorkspaceIcon extends StatefulWidget {
final double? emojiSize; final double? emojiSize;
final void Function(EmojiPickerResult) onSelected; final void Function(EmojiPickerResult) onSelected;
final double borderRadius; final double borderRadius;
final Alignment? alignment;
@override @override
State<WorkspaceIcon> createState() => _WorkspaceIconState(); State<WorkspaceIcon> createState() => _WorkspaceIconState();
@ -38,8 +40,8 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = widget.workspace.icon.isNotEmpty Widget child = widget.workspace.icon.isNotEmpty
? Container( ? Container(
width: widget.iconSize, width: widget.emojiSize ?? widget.iconSize,
alignment: Alignment.center, alignment: widget.alignment ?? Alignment.center,
child: FlowyText.emoji( child: FlowyText.emoji(
widget.workspace.icon, widget.workspace.icon,
fontSize: widget.emojiSize ?? widget.iconSize, fontSize: widget.emojiSize ?? widget.iconSize,

View File

@ -50,7 +50,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
UserSettingButton(userProfile: widget.userProfile), UserSettingButton(userProfile: widget.userProfile),
const HSpace(8.0), const HSpace(8.0),
const NotificationButton(), const NotificationButton(),
const HSpace(10.0), const HSpace(12.0),
], ],
); );
}, },

View File

@ -434,7 +434,6 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
child: FlowyText.regular( child: FlowyText.regular(
widget.view.name, widget.view.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
lineHeight: 1.1,
), ),
), ),
]; ];
@ -466,7 +465,6 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
child: Padding( child: Padding(
padding: EdgeInsets.only(left: widget.level * widget.leftPadding), padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: children, children: children,
), ),
), ),
@ -499,7 +497,6 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
? FlowyText.emoji( ? FlowyText.emoji(
widget.view.icon.value, widget.view.icon.value,
fontSize: 16.0, fontSize: 16.0,
lineHeight: 1.4,
) )
: Opacity(opacity: 0.6, child: widget.view.defaultIcon()); : Opacity(opacity: 0.6, child: widget.view.defaultIcon());

View File

@ -3,8 +3,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
const String _emojiFontFamily = 'noto color emoji';
class FlowyText extends StatelessWidget { class FlowyText extends StatelessWidget {
final String text; final String text;
final TextOverflow? overflow; final TextOverflow? overflow;
@ -138,16 +136,13 @@ class FlowyText extends StatelessWidget {
var fontFamily = this.fontFamily; var fontFamily = this.fontFamily;
var fallbackFontFamily = this.fallbackFontFamily; 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(); fontFamily = _loadEmojiFontFamilyIfNeeded();
if (fontFamily != null && fallbackFontFamily == null) { if (fontFamily != null && fallbackFontFamily == null) {
fallbackFontFamily = [fontFamily]; fallbackFontFamily = [fontFamily];
} }
}
var fontSize =
this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!;
if (Platform.isLinux && fontFamily == _emojiFontFamily) {
fontSize = fontSize * 0.8; fontSize = fontSize * 0.8;
} }
@ -175,6 +170,14 @@ class FlowyText extends StatelessWidget {
textAlign: textAlign, textAlign: textAlign,
overflow: overflow ?? TextOverflow.clip, overflow: overflow ?? TextOverflow.clip,
style: textStyle, 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() { String? _loadEmojiFontFamilyIfNeeded() {
if (Platform.isLinux || Platform.isAndroid) { if (_useNotoColorEmoji) {
return GoogleFonts.notoColorEmoji().fontFamily; return GoogleFonts.notoColorEmoji().fontFamily;
} }
return null; return null;
} }
bool get _useNotoColorEmoji =>
Platform.isLinux || Platform.isAndroid || Platform.isWindows;
} }

View File

@ -25,6 +25,7 @@ class FlowyTooltip extends StatelessWidget {
final isLightMode = Theme.of(context).brightness == Brightness.light; final isLightMode = Theme.of(context).brightness == Brightness.light;
return Tooltip( return Tooltip(
margin: margin, margin: margin,
verticalOffset: 16.0,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isLightMode ? const Color(0xE5171717) : const Color(0xE5E5E5E5), color: isLightMode ? const Color(0xE5171717) : const Color(0xE5E5E5E5),