diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index ff97b61241..fb224abe25 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -75,4 +75,15 @@ class KVKeys { /// /// The value is a double string. static const String scaleFactor = 'scaleFactor'; + + /// The key for saving the last opened space + /// + /// The value is a int string. + static const String lastOpenedSpace = 'lastOpenedSpace'; + + /// The key for saving the space order + /// + /// The value is a json string with the following format: + /// [0, 1, 2] + static const String spaceOrder = 'spaceOrder'; } 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 37a5cb0221..18c5c3a6df 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 @@ -23,7 +23,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.document_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.document_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Document), @@ -32,7 +32,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.grid_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.grid_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Grid), @@ -41,7 +41,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.board_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.board_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Board), @@ -49,8 +49,8 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { FlowyOptionTile.text( text: LocaleKeys.calendar_menuName.tr(), leftIcon: const FlowySvg( - FlowySvgs.date_s, - size: Size.square(20), + FlowySvgs.calendar_s, + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Calendar), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart index c1e2560e48..75b0151a3a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart @@ -1,9 +1,17 @@ +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/widgets/show_flowy_mobile_confirm_dialog.dart'; +import 'package:appflowy/startup/tasks/app_widget.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/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; enum MobileBottomSheetType { view, @@ -14,11 +22,13 @@ class MobileViewItemBottomSheet extends StatefulWidget { const MobileViewItemBottomSheet({ super.key, required this.view, + required this.actions, this.defaultType = MobileBottomSheetType.view, }); final ViewPB view; final MobileBottomSheetType defaultType; + final List actions; @override State createState() => @@ -27,12 +37,14 @@ class MobileViewItemBottomSheet extends StatefulWidget { class _MobileViewItemBottomSheetState extends State { MobileBottomSheetType type = MobileBottomSheetType.view; + final fToast = FToast(); @override void initState() { super.initState(); type = widget.defaultType; + fToast.init(AppGlobals.context); } @override @@ -40,6 +52,7 @@ class _MobileViewItemBottomSheetState extends State { switch (type) { case MobileBottomSheetType.view: return MobileViewItemBottomSheetBody( + actions: widget.actions, isFavorite: widget.view.isFavorite, onAction: (action) { switch (action) { @@ -59,7 +72,6 @@ class _MobileViewItemBottomSheetState extends State { case MobileViewItemBottomSheetBodyAction.delete: Navigator.pop(context); context.read().add(const ViewEvent.delete()); - break; case MobileViewItemBottomSheetBodyAction.addToFavorites: case MobileViewItemBottomSheetBodyAction.removeFromFavorites: @@ -68,6 +80,11 @@ class _MobileViewItemBottomSheetState extends State { .read() .add(FavoriteEvent.toggle(widget.view)); break; + case MobileViewItemBottomSheetBodyAction.removeFromRecent: + _removeFromRecent(context); + break; + case MobileViewItemBottomSheetBodyAction.divider: + break; } }, ); @@ -83,4 +100,74 @@ class _MobileViewItemBottomSheetState extends State { ); } } + + Future _removeFromRecent(BuildContext context) async { + final viewId = context.read().view.id; + final recentViewsBloc = context.read(); + Navigator.pop(context); + + await _showConfirmDialog( + onDelete: () { + recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId])); + + fToast.showToast( + child: const _RemoveToast(), + positionedToastBuilder: (context, child) { + return Positioned.fill( + top: 450, + child: child, + ); + }, + ); + }, + ); + } + + Future _showConfirmDialog({required VoidCallback onDelete}) async { + await showFlowyCupertinoConfirmDialog( + title: LocaleKeys.sideBar_removePageFromRecent.tr(), + leftButton: FlowyText.regular( + LocaleKeys.button_cancel.tr(), + color: const Color(0xFF1456F0), + ), + rightButton: FlowyText.medium( + LocaleKeys.button_delete.tr(), + color: const Color(0xFFFE0220), + ), + onRightButtonPressed: (context) { + onDelete(); + Navigator.pop(context); + }, + ); + } +} + +class _RemoveToast extends StatelessWidget { + const _RemoveToast(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xE5171717), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.success_s, + blendMode: null, + ), + const HSpace(8.0), + FlowyText.regular( + LocaleKeys.sideBar_removeSuccess.tr(), + fontSize: 16.0, + color: Colors.white, + ), + ], + ), + ); + } } 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 624ae33b9f..d9c6382288 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 @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,8 @@ enum MobileViewItemBottomSheetBodyAction { delete, addToFavorites, removeFromFavorites, + divider, + removeFromRecent, } class MobileViewItemBottomSheetBody extends StatelessWidget { @@ -18,63 +20,124 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { super.key, this.isFavorite = false, required this.onAction, + required this.actions, }); final bool isFavorite; final void Function(MobileViewItemBottomSheetBodyAction action) onAction; + final List actions; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MobileQuickActionButton( - text: LocaleKeys.button_rename.tr(), - icon: FlowySvgs.m_rename_s, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.rename, - ), - ), - _divider(), - MobileQuickActionButton( - text: isFavorite - ? LocaleKeys.button_removeFromFavorites.tr() - : LocaleKeys.button_addToFavorites.tr(), - icon: isFavorite - ? FlowySvgs.m_favorite_selected_lg - : FlowySvgs.m_favorite_unselected_lg, - iconColor: isFavorite ? Colors.yellow : null, - onTap: () => onAction( - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, - ), - ), - _divider(), - MobileQuickActionButton( - text: LocaleKeys.button_duplicate.tr(), - icon: FlowySvgs.m_duplicate_s, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.duplicate, - ), - ), - _divider(), - MobileQuickActionButton( - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - icon: FlowySvgs.m_delete_s, - iconColor: Theme.of(context).colorScheme.error, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.delete, - ), - ), - _divider(), - ], + children: + actions.map((action) => _buildActionButton(context, action)).toList(), ); } - Widget _divider() => const Divider( - height: 8.5, - thickness: 0.5, - ); + Widget _buildActionButton( + BuildContext context, + MobileViewItemBottomSheetBodyAction action, + ) { + switch (action) { + case MobileViewItemBottomSheetBodyAction.rename: + return FlowyOptionTile.text( + text: LocaleKeys.button_rename.tr(), + leftIcon: const FlowySvg( + FlowySvgs.view_item_rename_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.rename, + ), + ); + case MobileViewItemBottomSheetBodyAction.duplicate: + return FlowyOptionTile.text( + text: LocaleKeys.button_duplicate.tr(), + leftIcon: const FlowySvg( + FlowySvgs.duplicate_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.duplicate, + ), + ); + + case MobileViewItemBottomSheetBodyAction.share: + return FlowyOptionTile.text( + text: LocaleKeys.button_share.tr(), + leftIcon: const FlowySvg( + FlowySvgs.share_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.share, + ), + ); + case MobileViewItemBottomSheetBodyAction.delete: + return FlowyOptionTile.text( + text: LocaleKeys.button_delete.tr(), + textColor: Theme.of(context).colorScheme.error, + leftIcon: FlowySvg( + FlowySvgs.delete_s, + size: const Size.square(18), + color: Theme.of(context).colorScheme.error, + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.delete, + ), + ); + case MobileViewItemBottomSheetBodyAction.addToFavorites: + return FlowyOptionTile.text( + text: LocaleKeys.button_addToFavorites.tr(), + leftIcon: const FlowySvg( + FlowySvgs.favorite_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.addToFavorites, + ), + ); + case MobileViewItemBottomSheetBodyAction.removeFromFavorites: + return FlowyOptionTile.text( + text: LocaleKeys.button_removeFromFavorites.tr(), + leftIcon: const FlowySvg( + FlowySvgs.favorite_section_remove_from_favorite_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.removeFromFavorites, + ), + ); + case MobileViewItemBottomSheetBodyAction.removeFromRecent: + return FlowyOptionTile.text( + text: LocaleKeys.button_removeFromRecent.tr(), + leftIcon: const FlowySvg( + FlowySvgs.remove_from_recent_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.removeFromRecent, + ), + ); + + case MobileViewItemBottomSheetBodyAction.divider: + return const 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 80330de3c7..327db627c4 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 @@ -65,8 +65,21 @@ enum MobilePaneActionType { ], 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.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.delete, + ], ); }, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index f9fcba5754..ca012891f6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; @@ -10,6 +8,7 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -17,14 +16,15 @@ class MobileFavoritePageFolder extends StatelessWidget { const MobileFavoritePageFolder({ super.key, required this.userProfile, - required this.workspaceId, }); final UserProfilePB userProfile; - final String workspaceId; @override Widget build(BuildContext context) { + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; return MultiBlocProvider( providers: [ BlocProvider( @@ -67,7 +67,8 @@ class MobileFavoritePageFolder extends StatelessWidget { MobileFavoriteFolder( showHeader: false, forceExpanded: true, - views: favoriteState.views, + views: + favoriteState.views.map((e) => e.item).toList(), ), const VSpace(100.0), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index 7afc740b45..e6d2d895b1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -64,8 +64,6 @@ class MobileFavoriteScreen extends StatelessWidget { builder: (context, state) { return MobileFavoritePage( userProfile: userProfile, - workspaceId: state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, ); }, ), @@ -81,11 +79,9 @@ class MobileFavoritePage extends StatelessWidget { const MobileFavoritePage({ super.key, required this.userProfile, - required this.workspaceId, }); final UserProfilePB userProfile; - final String workspaceId; @override Widget build(BuildContext context) { @@ -108,7 +104,6 @@ class MobileFavoritePage extends StatelessWidget { Expanded( child: MobileFavoritePageFolder( userProfile: userProfile, - workspaceId: workspaceId, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart new file mode 100644 index 0000000000..f734690eb1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/prelude.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-user/protobuf.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileFavoriteSpace extends StatelessWidget { + const MobileFavoriteSpace({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add(SidebarSectionsEvent.initial(userProfile, workspaceId)), + ), + BlocProvider( + create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), + ), + ], + child: BlocListener( + listener: (context, state) => + context.read().add(const FavoriteEvent.initial()), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => + context.pushView(state.lastCreatedRootView!), + ), + ], + child: Builder( + builder: (context) { + final favoriteState = context.watch().state; + + if (favoriteState.isLoading) { + return const SizedBox.shrink(); + } + + if (favoriteState.views.isEmpty) { + return const EmptySpacePlaceholder( + type: MobileViewCardType.favorite, + ); + } + + return _FavoriteViews( + favoriteViews: favoriteState.views.reversed.toList(), + ); + }, + ), + ), + ), + ); + } +} + +class _FavoriteViews extends StatelessWidget { + const _FavoriteViews({ + required this.favoriteViews, + }); + + final List favoriteViews; + + @override + Widget build(BuildContext context) { + return Scrollbar( + child: ListView.separated( + key: const PageStorageKey('favorite_views_page_storage_key'), + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + ), + itemBuilder: (context, index) { + final view = favoriteViews[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(view.item.id), + view: view.item, + timestamp: view.timestamp, + type: MobileViewCardType.favorite, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: favoriteViews.length, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart new file mode 100644 index 0000000000..9ce6bbe91f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileHomeSpace extends StatelessWidget { + const MobileHomeSpace({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; + return Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + vertical: HomeSpaceViewSizes.mVerticalPadding, + ), + child: MobileFolders( + user: userProfile, + workspaceId: workspaceId, + showFavorite: false, + ), + ), + ), + ); + } +} 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 6e695ea0e4..100649701d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,5 +1,7 @@ +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/home/home.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; @@ -11,6 +13,7 @@ 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:go_router/go_router.dart'; // Contains Public And Private Sections class MobileFolders extends StatelessWidget { @@ -87,7 +90,8 @@ class MobileFolders extends StatelessWidget { views: state.section.publicViews, ), ], - const VSpace(8.0), + const VSpace(4.0), + const _TrashButton(), ], ), ); @@ -97,3 +101,28 @@ class MobileFolders extends StatelessWidget { ); } } + +class _TrashButton extends StatelessWidget { + const _TrashButton(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 52, + child: FlowyButton( + expand: true, + margin: const EdgeInsets.symmetric(vertical: 8), + leftIcon: const FlowySvg( + FlowySvgs.m_delete_s, + ), + leftIconSize: const Size.square(18), + iconPadding: 10.0, + text: FlowyText.regular( + LocaleKeys.trash_text.tr(), + fontSize: 16.0, + ), + onTap: () => context.push(MobileHomeTrashPage.routeName), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 69759fc508..e0b7c626d9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -1,23 +1,20 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/home.dart'; -import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; -import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; +import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; class MobileHomeScreen extends StatelessWidget { @@ -84,66 +81,49 @@ class MobileHomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add( - const UserWorkspaceEvent.initial(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), ), - child: BlocBuilder( + BlocProvider( + create: (context) => + FavoriteBloc()..add(const FavoriteEvent.initial()), + ), + ], + child: BlocConsumer( buildWhen: (previous, current) => previous.currentWorkspace?.workspaceId != current.currentWorkspace?.workspaceId, + listener: (context, state) => getIt().reset(), builder: (context, state) { if (state.currentWorkspace == null) { return const SizedBox.shrink(); } + return Column( children: [ // Header Padding( padding: EdgeInsets.only( - left: 16, - right: 16, + left: HomeSpaceViewSizes.mHorizontalPadding, + right: 8.0, top: Platform.isAndroid ? 8.0 : 0.0, ), child: MobileHomePageHeader( userProfile: userProfile, ), ), - const Divider(), - // Folder Expanded( - child: Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Recent files - const MobileRecentFolder(), - - // Folders - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: MobileFolders( - user: userProfile, - workspaceId: - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - showFavorite: false, - ), - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: _TrashButton(), - ), - ], - ), - ), + child: BlocProvider( + create: (context) => + SpaceOrderBloc()..add(const SpaceOrderEvent.initial()), + child: MobileSpaceTab( + userProfile: userProfile, ), ), ), @@ -154,25 +134,3 @@ class MobileHomePage extends StatelessWidget { ); } } - -class _TrashButton extends StatelessWidget { - const _TrashButton(); - - @override - Widget build(BuildContext context) { - return FlowyButton( - expand: true, - margin: const EdgeInsets.symmetric(vertical: 8), - leftIcon: FlowySvg( - FlowySvgs.m_delete_m, - color: Theme.of(context).colorScheme.onSurface, - ), - leftIconSize: const Size.square(24), - text: FlowyText.medium( - LocaleKeys.trash_text.tr(), - fontSize: 18.0, - ), - onTap: () => context.push(MobileHomeTrashPage.routeName), - ); - } -} 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 be47ef1b32..5ae2ff1430 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 @@ -35,7 +35,7 @@ class MobileHomePageHeader extends StatelessWidget { final isCollaborativeWorkspace = context.read().state.isCollabWorkspaceOn; return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 52), + constraints: const BoxConstraints(minHeight: 56), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -44,11 +44,14 @@ class MobileHomePageHeader extends StatelessWidget { ? _MobileWorkspace(userProfile: userProfile) : _MobileUser(userProfile: userProfile), ), - IconButton( - onPressed: () => context.push( + GestureDetector( + onTap: () => context.push( MobileHomeSettingPage.routeName, ), - icon: const FlowySvg(FlowySvgs.m_setting_m), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: FlowySvg(FlowySvgs.m_setting_m), + ), ), ], ), @@ -119,7 +122,6 @@ class _MobileWorkspace extends StatelessWidget { }, child: Row( children: [ - const HSpace(2.0), SizedBox.square( dimension: 34.0, child: WorkspaceIcon( @@ -142,7 +144,7 @@ class _MobileWorkspace extends StatelessWidget { children: [ Row( children: [ - FlowyText.medium( + FlowyText.semibold( currentWorkspace.name, fontSize: 16.0, overflow: TextOverflow.ellipsis, @@ -151,7 +153,7 @@ class _MobileWorkspace extends StatelessWidget { const FlowySvg(FlowySvgs.list_dropdown_s), ], ), - FlowyText.medium( + FlowyText.regular( userProfile.email.isNotEmpty ? userProfile.email : userProfile.name, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index fa585903f3..a2b9ae52c7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -38,7 +38,8 @@ class _MobileRecentFolderState extends State { builder: (context, state) { final ids = {}; - List recentViews = state.views.reversed.toList(); + List recentViews = + state.views.reversed.map((e) => e.item).toList(); recentViews.retainWhere((element) => ids.add(element.id)); // only keep the first 20 items. 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 new file mode 100644 index 0000000000..a9dc41d093 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart @@ -0,0 +1,86 @@ +import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:appflowy/workspace/application/recent/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +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'; + +class MobileRecentSpace extends StatelessWidget { + const MobileRecentSpace({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + RecentViewsBloc()..add(const RecentViewsEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const SizedBox.shrink(); + } + + final recentViews = _filterRecentViews(state.views); + + if (recentViews.isEmpty) { + return const Center( + child: EmptySpacePlaceholder(type: MobileViewCardType.recent), + ); + } + + return _RecentViews(recentViews: recentViews); + }, + ), + ); + } + + List _filterRecentViews(List recentViews) { + final ids = {}; + final filteredRecentViews = recentViews.reversed.toList(); + filteredRecentViews.retainWhere((e) => ids.add(e.item.id)); + return filteredRecentViews; + } +} + +class _RecentViews extends StatelessWidget { + const _RecentViews({ + required this.recentViews, + }); + + final List recentViews; + + @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, + ), + ), + ), + 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 4d9d109d3f..96640522cb 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 @@ -9,7 +9,6 @@ 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'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -36,29 +35,28 @@ class MobileSectionFolder extends StatelessWidget { builder: (context, state) { return Column( children: [ - MobileSectionFolderHeader( - title: title, - isExpanded: context.read().state.isExpanded, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - index: 0, - viewSection: spaceType.toViewSectionPB, - ), - ); - context.read().add( - const FolderEvent.expandOrUnExpand(isExpanded: true), - ); - }, - ), - const VSpace(8.0), - const Divider( - height: 1, + SizedBox( + height: HomeSpaceViewSizes.mViewHeight, + child: MobileSectionFolderHeader( + title: title, + isExpanded: context.read().state.isExpanded, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: LocaleKeys.menuAppHeader_defaultNewPageName + .tr(), + index: 0, + viewSection: spaceType.toViewSectionPB, + ), + ); + context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ); + }, + ), ), if (state.isExpanded) ...views.map( 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 3ba15df25d..49a9829d8a 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 @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -29,24 +30,23 @@ class _MobileSectionFolderHeaderState extends State { @override Widget build(BuildContext context) { - const iconSize = 32.0; return Row( children: [ Expanded( child: FlowyButton( - text: FlowyText.semibold( + text: FlowyText.medium( widget.title, - fontSize: 20.0, + fontSize: 16.0, ), margin: const EdgeInsets.symmetric(vertical: 8), expandText: false, + iconPadding: 2, mainAxisAlignment: MainAxisAlignment.start, rightIcon: AnimatedRotation( duration: const Duration(milliseconds: 200), turns: _turns, - child: const Icon( - Icons.keyboard_arrow_down_rounded, - color: Colors.grey, + child: const FlowySvg( + FlowySvgs.m_spaces_expand_s, ), ), onTap: () { @@ -60,12 +60,10 @@ class _MobileSectionFolderHeaderState extends State { FlowyIconButton( key: mobileCreateNewPageButtonKey, hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, + height: HomeSpaceViewSizes.mViewButtonDimension, + width: HomeSpaceViewSizes.mViewButtonDimension, icon: const FlowySvg( - FlowySvgs.add_s, - size: Size.square(iconSize), + FlowySvgs.m_space_add_s, ), onPressed: widget.onAdded, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart new file mode 100644 index 0000000000..e59fa87538 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart @@ -0,0 +1,55 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class EmptySpacePlaceholder extends StatelessWidget { + const EmptySpacePlaceholder({super.key, required this.type}); + + final MobileViewCardType type; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + FlowySvgs.m_empty_page_xl, + ), + const VSpace(16.0), + FlowyText.medium( + _emptyPageText, + fontSize: 18.0, + textAlign: TextAlign.center, + ), + const VSpace(8.0), + FlowyText.regular( + _emptyPageSubText, + fontSize: 17.0, + maxLines: 10, + textAlign: TextAlign.center, + lineHeight: 1.3, + color: Theme.of(context).hintColor, + ), + ], + ), + ); + } + + String get _emptyPageText => switch (type) { + MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(), + MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(), + }; + + String get _emptyPageSubText => switch (type) { + MobileViewCardType.recent => + LocaleKeys.sideBar_emptyRecentDescription.tr(), + MobileViewCardType.favorite => + LocaleKeys.sideBar_emptyFavoriteDescription.tr(), + }; +} 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 new file mode 100644 index 0000000000..2b22a185f6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart @@ -0,0 +1,396 @@ +import 'dart:io'; + +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/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/flowy_gradient_colors.dart'; +import 'package:appflowy/util/string_extension.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/view/view_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:string_validator/string_validator.dart'; + +enum MobileViewCardType { + recent, + favorite; + + String get lastOperationHintText => switch (this) { + MobileViewCardType.recent => LocaleKeys.sideBar_lastViewed.tr(), + MobileViewCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(), + }; +} + +class MobileViewCard extends StatelessWidget { + const MobileViewCard({ + super.key, + required this.view, + this.timestamp, + required this.type, + }); + + final ViewPB view; + final Int64? timestamp; + final MobileViewCardType type; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + ViewBloc(view: view)..add(const ViewEvent.initial()), + ), + BlocProvider( + create: (context) => + RecentViewBloc(view: view)..add(const RecentViewEvent.initial()), + ), + ], + 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), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildDescription(BuildContext context, RecentViewState state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // page icon & page title + _buildTitle(context, state), + const VSpace(12.0), + // author & last viewed + _buildNameAndLastViewed(context, state), + ], + ); + } + + Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) { + final supportAvatar = isURL(state.icon); + if (!supportAvatar) { + return _buildLastViewed(context); + } + return Row( + children: [ + _buildAvatar(context, state), + Flexible(child: _buildAuthor(context, state)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 3.0), + child: FlowySvg(FlowySvgs.dot_s), + ), + _buildLastViewed(context), + ], + ); + } + + Widget _buildAvatar(BuildContext context, RecentViewState state) { + final userProfile = Provider.of(context); + final iconUrl = userProfile?.iconUrl; + if (iconUrl == null || + iconUrl.isEmpty || + view.createdBy != userProfile?.id) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 2, bottom: 2, right: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox.square( + dimension: 16.0, + child: FlowyNetworkImage( + url: iconUrl, + ), + ), + ), + ); + } + + Widget _buildCover(BuildContext context, RecentViewState state) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _ViewCover( + coverTypeV1: state.coverTypeV1, + coverTypeV2: state.coverTypeV2, + value: state.coverValue, + ), + ); + } + + Widget _buildTitle(BuildContext context, RecentViewState state) { + final name = state.name; + final icon = state.icon; + return RichText( + maxLines: 3, + overflow: TextOverflow.ellipsis, + text: TextSpan( + children: [ + TextSpan( + text: icon, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 17.0, + fontWeight: FontWeight.w600, + fontFamily: GoogleFonts.notoColorEmoji().fontFamily, + ), + ), + const TextSpan(text: ' '), + TextSpan( + text: name, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildAuthor(BuildContext context, RecentViewState state) { + return FlowyText.regular( + // view.createdBy.toString(), + 'Lucas', + fontSize: 12.0, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildLastViewed(BuildContext context) { + if (timestamp == null) { + return const SizedBox.shrink(); + } + final date = _formatTimestamp( + timestamp!.toInt() * 1000, + ); + return FlowyText.regular( + date, + fontSize: 12.0, + color: Theme.of(context).hintColor, + ); + } + + String _formatTimestamp(int timestamp) { + final now = DateTime.now(); + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + final difference = now.difference(dateTime); + final String date; + + if (difference.inMinutes < 1) { + date = LocaleKeys.sideBar_justNow.tr(); + } else if (difference.inHours < 1) { + // Less than 1 hour + date = LocaleKeys.sideBar_minutesAgo + .tr(namedArgs: {'count': difference.inMinutes.toString()}); + } else if (difference.inHours >= 1 && difference.inHours < 24) { + // Between 1 hour and 24 hours + date = DateFormat('h:mm a').format(dateTime); + } else if (difference.inDays >= 1 && dateTime.year == now.year) { + // More than 24 hours but within the current year + date = DateFormat('M/d, h:mm a').format(dateTime); + } else { + // Other cases (previous years) + date = DateFormat('M/d/yyyy, h:mm a').format(dateTime); + } + + if (difference.inHours >= 1) { + return '${type.lastOperationHintText} $date'; + } + + return date; + } + + Future _showActionSheet(BuildContext context) async { + final viewBloc = context.read(); + final favoriteBloc = context.read(); + final recentViewsBloc = context.read(); + await showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + backgroundColor: AFThemeExtension.of(context).background, + useRootNavigator: true, + 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: _buildActions(isFavorite), + ); + }, + ), + ); + }, + ); + } + + List _buildActions(bool isFavorite) { + switch (type) { + case MobileViewCardType.recent: + return [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.removeFromRecent, + ]; + case MobileViewCardType.favorite: + return [ + MobileViewItemBottomSheetBodyAction.removeFromFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + ]; + } + } +} + +class _ViewCover extends StatelessWidget { + const _ViewCover({ + required this.coverTypeV1, + this.coverTypeV2, + this.value, + }); + + final CoverType coverTypeV1; + final PageStyleCoverImageType? coverTypeV2; + final String? value; + + @override + Widget build(BuildContext context) { + final placeholder = Container( + color: const Color(0xFFE1FBFF), + ); + final value = this.value; + if (value == null) { + return placeholder; + } + if (coverTypeV2 != null) { + return _buildCoverV2(context, value, placeholder); + } + return _buildCoverV1(context, value, placeholder); + } + + Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) { + final type = coverTypeV2; + if (type == null) { + return placeholder; + } + if (type == PageStyleCoverImageType.customImage || + type == PageStyleCoverImageType.unsplashImage) { + final userProfilePB = Provider.of(context); + return FlowyNetworkImage( + url: value, + userProfilePB: userProfilePB, + ); + } + + if (type == PageStyleCoverImageType.builtInImage) { + return Image.asset( + PageStyleCoverImageType.builtInImagePath(value), + fit: BoxFit.cover, + ); + } + + if (type == PageStyleCoverImageType.pureColor) { + final color = value.coverColor(context); + if (color != null) { + return ColoredBox( + color: color, + ); + } + } + + if (type == PageStyleCoverImageType.gradientColor) { + return Container( + decoration: BoxDecoration( + gradient: FlowyGradientColor.fromId(value).linear, + ), + ); + } + + if (type == PageStyleCoverImageType.localImage) { + return Image.file( + File(value), + fit: BoxFit.cover, + ); + } + + return placeholder; + } + + Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) { + switch (coverTypeV1) { + case CoverType.file: + if (isURL(value)) { + final userProfilePB = Provider.of(context); + return FlowyNetworkImage( + url: value, + userProfilePB: userProfilePB, + ); + } + final imageFile = File(value); + if (!imageFile.existsSync()) { + return placeholder; + } + return Image.file( + imageFile, + ); + case CoverType.asset: + return Image.asset( + value, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = value.tryToColor() ?? Colors.white; + return Container( + color: color, + ); + case CoverType.none: + return placeholder; + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart new file mode 100644 index 0000000000..1a3eb121f3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +class RoundUnderlineTabIndicator extends Decoration { + const RoundUnderlineTabIndicator({ + this.borderRadius, + this.borderSide = const BorderSide(width: 2.0, color: Colors.white), + this.insets = EdgeInsets.zero, + required this.width, + }); + + final BorderRadius? borderRadius; + final BorderSide borderSide; + final EdgeInsetsGeometry insets; + final double width; + + @override + Decoration? lerpFrom(Decoration? a, double t) { + if (a is UnderlineTabIndicator) { + return UnderlineTabIndicator( + borderSide: BorderSide.lerp(a.borderSide, borderSide, t), + insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!, + ); + } + return super.lerpFrom(a, t); + } + + @override + Decoration? lerpTo(Decoration? b, double t) { + if (b is UnderlineTabIndicator) { + return UnderlineTabIndicator( + borderSide: BorderSide.lerp(borderSide, b.borderSide, t), + insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, + ); + } + return super.lerpTo(b, t); + } + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _UnderlinePainter(this, borderRadius, onChanged); + } + + Rect _indicatorRectFor(Rect rect, TextDirection textDirection) { + final Rect indicator = insets.resolve(textDirection).deflateRect(rect); + final center = indicator.center.dx; + return Rect.fromLTWH( + center - width / 2.0, + indicator.bottom - borderSide.width, + width, + borderSide.width, + ); + } + + @override + Path getClipPath(Rect rect, TextDirection textDirection) { + if (borderRadius != null) { + return Path() + ..addRRect( + borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)), + ); + } + return Path()..addRect(_indicatorRectFor(rect, textDirection)); + } +} + +class _UnderlinePainter extends BoxPainter { + _UnderlinePainter( + this.decoration, + this.borderRadius, + super.onChanged, + ); + + final RoundUnderlineTabIndicator decoration; + final BorderRadius? borderRadius; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + assert(configuration.size != null); + final Rect rect = offset & configuration.size!; + final TextDirection textDirection = configuration.textDirection!; + final Paint paint; + if (borderRadius != null) { + paint = Paint()..color = decoration.borderSide.color; + final Rect indicator = decoration._indicatorRectFor(rect, textDirection); + final RRect rrect = RRect.fromRectAndCorners( + indicator, + topLeft: borderRadius!.topLeft, + topRight: borderRadius!.topRight, + bottomRight: borderRadius!.bottomRight, + bottomLeft: borderRadius!.bottomLeft, + ); + canvas.drawRRect(rrect, paint); + } else { + paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round; + final Rect indicator = decoration + ._indicatorRectFor(rect, textDirection) + .deflate(decoration.borderSide.width / 2.0); + canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint); + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart new file mode 100644 index 0000000000..24add88427 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart @@ -0,0 +1,56 @@ +import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:reorderable_tabbar/reorderable_tabbar.dart'; + +class MobileSpaceTabBar extends StatelessWidget { + const MobileSpaceTabBar({ + super.key, + this.height = 38.0, + required this.tabController, + required this.tabs, + required this.onReorder, + }); + + final double height; + final List tabs; + final TabController tabController; + final OnReorder onReorder; + + @override + Widget build(BuildContext context) { + final baseStyle = Theme.of(context).textTheme.bodyMedium; + final labelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 15.0, + ); + final unselectedLabelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 15.0, + ); + + return Container( + height: height, + padding: const EdgeInsets.only(left: 8.0), + child: ReorderableTabBar( + controller: tabController, + tabs: tabs.map((e) => Tab(text: e.tr)).toList(), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).primaryColor, + isScrollable: true, + labelStyle: labelStyle, + labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), + unselectedLabelStyle: unselectedLabelStyle, + overlayColor: WidgetStateProperty.all(Colors.transparent), + indicator: RoundUnderlineTabIndicator( + width: 28.0, + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: 3, + ), + ), + onReorder: onReorder, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart new file mode 100644 index 0000000000..097bd22910 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -0,0 +1,107 @@ +import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart'; +import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart'; +import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart'; +import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/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:provider/provider.dart'; + +class MobileSpaceTab extends StatefulWidget { + const MobileSpaceTab({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + State createState() => _MobileSpaceTabState(); +} + +class _MobileSpaceTabState extends State + with SingleTickerProviderStateMixin { + TabController? tabController; + + @override + void dispose() { + tabController?.removeListener(_onTabChange); + tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Provider.value( + value: widget.userProfile, + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const SizedBox.shrink(); + } + + _initTabController(state); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MobileSpaceTabBar( + tabController: tabController!, + tabs: state.tabsOrder, + onReorder: (from, to) { + context.read().add( + SpaceOrderEvent.reorder(from, to), + ); + }, + ), + const HSpace(12.0), + Expanded( + child: TabBarView( + controller: tabController, + children: _buildTabs(state), + ), + ), + ], + ); + }, + ), + ); + } + + void _initTabController(SpaceOrderState state) { + if (tabController != null) { + return; + } + tabController = TabController( + length: state.tabsOrder.length, + vsync: this, + initialIndex: state.tabsOrder.indexOf(state.defaultTab), + ); + tabController?.addListener(_onTabChange); + } + + void _onTabChange() { + if (tabController == null) { + return; + } + context.read().add( + SpaceOrderEvent.open( + tabController!.index, + ), + ); + } + + List _buildTabs(SpaceOrderState state) { + return state.tabsOrder.map((tab) { + switch (tab) { + case MobileSpaceTabType.recent: + return const MobileRecentSpace(); + case MobileSpaceTabType.spaces: + return MobileHomeSpace(userProfile: widget.userProfile); + case MobileSpaceTabType.favorites: + return MobileFavoriteSpace(userProfile: widget.userProfile); + default: + throw Exception('Unknown tab type: $tab'); + } + }).toList(); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart new file mode 100644 index 0000000000..e3c1439dd4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'space_order_bloc.freezed.dart'; + +enum MobileSpaceTabType { + // DO NOT CHANGE THE ORDER + spaces, + recent, + favorites; + + String get tr { + switch (this) { + case MobileSpaceTabType.recent: + return LocaleKeys.sideBar_RecentSpace.tr(); + case MobileSpaceTabType.spaces: + return LocaleKeys.sideBar_Spaces.tr(); + case MobileSpaceTabType.favorites: + return LocaleKeys.sideBar_favoriteSpace.tr(); + } + } +} + +class SpaceOrderBloc extends Bloc { + SpaceOrderBloc() : super(const SpaceOrderState()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final tabsOrder = await _getTabsOrder(); + final defaultTab = await _getDefaultTab(); + emit( + state.copyWith( + tabsOrder: tabsOrder, + defaultTab: defaultTab, + isLoading: false, + ), + ); + }, + open: (index) async { + final tab = state.tabsOrder[index]; + await _setDefaultTab(tab); + }, + reorder: (from, to) async { + final tabsOrder = List.of(state.tabsOrder); + tabsOrder.insert(to, tabsOrder.removeAt(from)); + await _setTabsOrder(tabsOrder); + emit(state.copyWith(tabsOrder: tabsOrder)); + }, + ); + }, + ); + } + + final _storage = getIt(); + + Future _getDefaultTab() async { + try { + return await _storage.getWithFormat( + KVKeys.lastOpenedSpace, (value) { + return MobileSpaceTabType.values[int.parse(value)]; + }) ?? + MobileSpaceTabType.spaces; + } catch (e) { + return MobileSpaceTabType.spaces; + } + } + + Future _setDefaultTab(MobileSpaceTabType tab) async { + await _storage.set( + KVKeys.lastOpenedSpace, + tab.index.toString(), + ); + } + + Future> _getTabsOrder() async { + try { + return await _storage.getWithFormat>( + KVKeys.spaceOrder, (value) { + final order = jsonDecode(value).cast(); + if (order.isEmpty) { + return MobileSpaceTabType.values; + } + return order + .map((e) => MobileSpaceTabType.values[e]) + .cast() + .toList(); + }) ?? + MobileSpaceTabType.values; + } catch (e) { + return MobileSpaceTabType.values; + } + } + + Future _setTabsOrder(List tabsOrder) async { + await _storage.set( + KVKeys.spaceOrder, + jsonEncode(tabsOrder.map((e) => e.index).toList()), + ); + } +} + +@freezed +class SpaceOrderEvent with _$SpaceOrderEvent { + const factory SpaceOrderEvent.initial() = Initial; + const factory SpaceOrderEvent.open(int index) = Open; + const factory SpaceOrderEvent.reorder(int from, int to) = Reorder; +} + +@freezed +class SpaceOrderState with _$SpaceOrderState { + const factory SpaceOrderState({ + @Default(MobileSpaceTabType.spaces) MobileSpaceTabType defaultTab, + @Default(MobileSpaceTabType.values) List tabsOrder, + @Default(true) bool isLoading, + }) = _SpaceOrderState; + + factory SpaceOrderState.initial() => const SpaceOrderState(); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 101a546294..1849388db4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -20,49 +20,40 @@ class MobileBottomNavigationBar extends StatelessWidget { return Scaffold( body: navigationShell, - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - enableFeedback: true, - type: BottomNavigationBarType.fixed, - items: [ - BottomNavigationBarItem( - // There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here. - label: 'home', - icon: const FlowySvg(FlowySvgs.m_home_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_home_selected_lg, - color: style.colorScheme.primary, + bottomNavigationBar: Theme( + data: ThemeData( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + enableFeedback: false, + type: BottomNavigationBarType.fixed, + elevation: 0, + items: [ + const BottomNavigationBarItem( + label: 'home', + icon: FlowySvg(FlowySvgs.m_home_unselected_m), + activeIcon: + FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null), ), - ), - const BottomNavigationBarItem( - label: 'favorite', - icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_favorite_selected_lg, - blendMode: null, + const BottomNavigationBarItem( + label: 'add', + icon: FlowySvg(FlowySvgs.m_home_add_m), ), - ), - // Enable this when search is ready. - // BottomNavigationBarItem( - // label: 'search', - // icon: const FlowySvg(FlowySvgs.m_search_lg), - // activeIcon: FlowySvg( - // FlowySvgs.m_search_lg, - // color: style.colorScheme.primary, - // ), - // ), - BottomNavigationBarItem( - label: 'notification', - icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_notification_selected_lg, - color: style.colorScheme.primary, + BottomNavigationBarItem( + label: 'notification', + icon: const FlowySvg(FlowySvgs.m_home_notification_m), + activeIcon: FlowySvg( + FlowySvgs.m_home_notification_m, + color: style.colorScheme.primary, + ), ), - ), - ], - currentIndex: navigationShell.currentIndex, - onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + ), ), ); } 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 862ce794b6..021c671ce9 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,11 +1,13 @@ +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/plugins/base/emoji/emoji_text.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'; @@ -17,8 +19,6 @@ import 'package:flutter_slidable/flutter_slidable.dart'; typedef ViewItemOnSelected = void Function(ViewPB); typedef ActionPaneBuilder = ActionPane Function(BuildContext context); -const _itemHeight = 48.0; - class MobileViewItem extends StatelessWidget { const MobileViewItem({ super.key, @@ -177,48 +177,10 @@ class InnerMobileViewItem extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ child, - const Divider( - height: 1, - ), ...children, ], ); - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const Divider( - height: 1, - ), - Container( - height: _itemHeight, - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: (level + 2) * leftPadding), - child: FlowyText.medium( - LocaleKeys.noPagesInside.tr(), - color: Colors.grey, - ), - ), - ), - const Divider( - height: 1, - ), - ], - ); } - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - const Divider( - height: 1, - ), - ], - ); } // wrap the child with DraggableItem if isDraggable is true @@ -226,7 +188,6 @@ class InnerMobileViewItem extends StatelessWidget { child = DraggableViewItem( isFirstChild: isFirstChild, view: view, - // FIXME: use better color centerHighlightColor: Colors.blue.shade200, topHighlightColor: Colors.blue.shade200, bottomHighlightColor: Colors.blue.shade200, @@ -296,15 +257,15 @@ class _SingleMobileInnerViewItemState extends State { final children = [ // expand icon _buildLeftIcon(), - const HSpace(4), + const HSpace(6), // icon _buildViewIcon(), const HSpace(8), // title Expanded( - child: FlowyText.medium( + child: FlowyText.regular( widget.view.name, - fontSize: 18.0, + fontSize: 16.0, overflow: TextOverflow.ellipsis, ), ), @@ -317,6 +278,7 @@ class _SingleMobileInnerViewItemState extends State { // only support add button for document layout if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) { // + button + children.add(_buildViewMoreButton(context)); children.add(_buildViewAddButton(context)); } @@ -324,7 +286,7 @@ class _SingleMobileInnerViewItemState extends State { borderRadius: BorderRadius.circular(4.0), onTap: () => widget.onSelected(widget.view), child: SizedBox( - height: _itemHeight, + height: HomeSpaceViewSizes.mViewHeight, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( @@ -349,12 +311,12 @@ class _SingleMobileInnerViewItemState extends State { Widget _buildViewIcon() { final icon = widget.view.icon.value.isNotEmpty - ? EmojiText( - emoji: widget.view.icon.value, - fontSize: 24.0, + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 20.0, ) : SizedBox.square( - dimension: 26.0, + dimension: 18.0, child: widget.view.defaultIcon(), ); return icon; @@ -368,13 +330,17 @@ class _SingleMobileInnerViewItemState extends State { return const _DotIconWidget(); } + if (context.read().state.view.childViews.isEmpty) { + return HSpace(widget.leftPadding); + } + return GestureDetector( child: AnimatedRotation( duration: const Duration(milliseconds: 250), turns: widget.isExpanded ? 0 : -0.25, - child: const Icon( - Icons.keyboard_arrow_down_rounded, - size: 28, + child: const FlowySvg( + FlowySvgs.m_expand_s, + blendMode: null, ), ), onTap: () { @@ -418,6 +384,51 @@ class _SingleMobileInnerViewItemState extends State { }, ); } + + // + 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, + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.delete, + ], + ); + }, + ), + ); + }, + ); + } } class _DotIconWidget extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart index eb2f8ea9f8..77bb57773f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart @@ -1,9 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const _iconSize = 32.0; - class MobileViewAddButton extends StatelessWidget { const MobileViewAddButton({ super.key, @@ -15,12 +14,31 @@ class MobileViewAddButton extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyIconButton( - iconPadding: const EdgeInsets.all(2), - width: _iconSize, - height: _iconSize, + width: HomeSpaceViewSizes.mViewButtonDimension, + height: HomeSpaceViewSizes.mViewButtonDimension, icon: const FlowySvg( - FlowySvgs.add_s, - size: Size.square(_iconSize), + FlowySvgs.m_space_add_s, + ), + onPressed: onPressed, + ); + } +} + +class MobileViewMoreButton extends StatelessWidget { + const MobileViewMoreButton({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + width: HomeSpaceViewSizes.mViewButtonDimension, + height: HomeSpaceViewSizes.mViewButtonDimension, + icon: const FlowySvg( + FlowySvgs.m_space_more_s, ), onPressed: onPressed, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index ba2d2b00cc..5be3d3e78c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class MobileQuickActionButton extends StatelessWidget { const MobileQuickActionButton({ @@ -32,20 +31,20 @@ class MobileQuickActionButton extends StatelessWidget { enable ? null : const WidgetStatePropertyAll(Colors.transparent), splashColor: Colors.transparent, child: Container( - height: 44, + height: 52, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ FlowySvg( icon, - size: const Size.square(20), + size: const Size.square(18), color: enable ? iconColor : Theme.of(context).disabledColor, ), const HSpace(12), Expanded( - child: FlowyText( + child: FlowyText.regular( text, - fontSize: 15, + fontSize: 16, color: enable ? textColor : Theme.of(context).disabledColor, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index 5a481eaa68..321632a36a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; enum ConfirmDialogActionAlignment { @@ -85,3 +87,46 @@ Future showFlowyMobileConfirmDialog( }, ); } + +Future showFlowyCupertinoConfirmDialog({ + BuildContext? context, + required String title, + required Widget leftButton, + required Widget rightButton, + void Function(BuildContext context)? onLeftButtonPressed, + void Function(BuildContext context)? onRightButtonPressed, +}) { + return showDialog( + context: context ?? AppGlobals.context, + builder: (context) => CupertinoAlertDialog( + title: FlowyText.medium( + title, + fontSize: 18, + maxLines: 10, + lineHeight: 1.3, + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + if (onLeftButtonPressed != null) { + onLeftButtonPressed(context); + } else { + Navigator.of(context).pop(); + } + }, + child: leftButton, + ), + CupertinoDialogAction( + onPressed: () { + if (onRightButtonPressed != null) { + onRightButtonPressed(context); + } else { + Navigator.of(context).pop(); + } + }, + child: rightButton, + ), + ], + ), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5f2d6e5c70..fbb60ce1de 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -101,9 +101,13 @@ Map getEditorBuilderMap({ return const EdgeInsets.only(top: 12.0, bottom: 4.0); }, - placeholderText: (node) => LocaleKeys.blockPlaceholders_heading.tr( - args: [node.attributes[HeadingBlockKeys.level].toString()], - ), + placeholderText: (node) { + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return LocaleKeys.blockPlaceholders_heading.tr( + args: [level.toString()], + ); + }, ), textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index 741b3c16ee..6efdd815bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -16,6 +16,7 @@ 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-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flutter/material.dart'; @@ -133,9 +134,11 @@ class _DocumentImmersiveCoverState extends State { if (documentFontFamily != null && fontFamily != documentFontFamily) { fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily; } - return TextField( + + return AutoSizeTextField( controller: textEditingController, focusNode: focusNode, + minFontSize: 18.0, decoration: const InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, @@ -151,6 +154,7 @@ class _DocumentImmersiveCoverState extends State { fontFamily: fontFamily, color: state.cover.isNone || state.cover.isPresets ? null : Colors.white, + overflow: TextOverflow.ellipsis, ), onChanged: _rename, onSubmitted: _rename, diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 0bbfd3359d..4da27109d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; @@ -20,6 +18,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; // const _channel = "InlinePageReference"; @@ -65,8 +64,11 @@ class InlinePageReferenceService extends InlineActionsDelegate { _recentViewsInitialized = true; - final views = - (await _recentService.recentViews()).reversed.toSet().toList(); + final views = (await _recentService.recentViews()) + .reversed + .map((e) => e.item) + .toSet() + .toList(); // Filter by viewLayout views.retainWhere( diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 9e719d6ecc..f90685f030 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -253,9 +253,9 @@ class _ApplicationWidgetState extends State { } class AppGlobals { - // static GlobalKey scaffoldMessengerKey = GlobalKey(); static GlobalKey rootNavKey = GlobalKey(); static NavigatorState get nav => rootNavKey.currentState!; + static BuildContext get context => rootNavKey.currentContext!; } class ApplicationBlocObserver extends BlocObserver { diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart index 6ce62acae8..8bc1e549ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -40,17 +40,20 @@ class FavoriteBloc extends Bloc { emit( result.fold( (favoriteViews) { - final views = favoriteViews.items.map((v) => v.item).toList(); - final pinnedViews = views.where((v) => v.isPinned).toList(); + final views = favoriteViews.items.toList(); + final pinnedViews = + views.where((v) => v.item.isPinned).toList(); final unpinnedViews = - views.where((v) => !v.isPinned).toList(); + views.where((v) => !v.item.isPinned).toList(); return state.copyWith( + isLoading: false, views: views, pinnedViews: pinnedViews, unpinnedViews: unpinnedViews, ); }, (error) => state.copyWith( + isLoading: false, views: [], ), ), @@ -105,12 +108,11 @@ class FavoriteEvent with _$FavoriteEvent { @freezed class FavoriteState with _$FavoriteState { const factory FavoriteState({ - required List views, - @Default([]) List pinnedViews, - @Default([]) List unpinnedViews, + @Default([]) List views, + @Default([]) List pinnedViews, + @Default([]) List unpinnedViews, + @Default(true) bool isLoading, }) = _FavoriteState; - factory FavoriteState.initial() => const FavoriteState( - views: [], - ); + factory FavoriteState.initial() => const FavoriteState(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart index bfd4d654a3..47491e8773 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/recent/recent_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/foundation.dart'; /// This is a lazy-singleton to share recent views across the application. /// @@ -23,21 +22,23 @@ class CachedRecentService { Completer _completer = Completer(); - ValueNotifier> notifier = ValueNotifier(const []); + ValueNotifier> notifier = ValueNotifier(const []); - List get _recentViews => notifier.value; - set _recentViews(List value) => notifier.value = value; + List get _recentViews => notifier.value; + set _recentViews(List value) => notifier.value = value; final _listener = RecentViewsListener(); - Future> recentViews() async { + Future> recentViews() async { if (_isInitialized) return _recentViews; _isInitialized = true; _listener.start(recentViewsUpdated: _recentViewsUpdated); - final result = await _readRecentViews(); - _recentViews = result.toNullable()?.items ?? const []; + _recentViews = await _readRecentViews().fold( + (s) => s.items, + (_) => [], + ); _completer.complete(); return _recentViews; @@ -55,7 +56,7 @@ class CachedRecentService { ), ).send(); - Future> _readRecentViews() => + Future> _readRecentViews() => FolderEventReadRecentViews().send(); bool _isInitialized = false; @@ -74,11 +75,12 @@ class CachedRecentService { void _recentViewsUpdated( FlowyResult result, - ) { + ) async { final viewIds = result.toNullable(); if (viewIds != null) { - _readRecentViews().then( - (views) => _recentViews = views.toNullable()?.items ?? const [], + _recentViews = await _readRecentViews().fold( + (s) => s.items, + (_) => [], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart index b43edfa2b1..d67c24e854 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart @@ -35,7 +35,12 @@ class RecentViewsBloc extends Bloc { await _service.updateRecentViews(e.viewIds, false); }, fetchRecentViews: (e) async { - emit(state.copyWith(views: await _service.recentViews())); + emit( + state.copyWith( + isLoading: false, + views: await _service.recentViews(), + ), + ); }, resetRecentViews: (e) async { await _service.reset(); @@ -63,8 +68,10 @@ class RecentViewsEvent with _$RecentViewsEvent { @freezed class RecentViewsState with _$RecentViewsState { - const factory RecentViewsState({required List views}) = - _RecentViewsState; + const factory RecentViewsState({ + required List views, + @Default(true) bool isLoading, + }) = _RecentViewsState; factory RecentViewsState.initial() => const RecentViewsState(views: []); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index c0a885c19b..ec1a2c43d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/user/application/user_listener.dart'; @@ -12,6 +10,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -51,6 +50,7 @@ class UserWorkspaceBloc extends Bloc { 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' 'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn', ); + final members = await _fetchMembers(currentWorkspace?.workspaceId); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); await _userService.openWorkspace(currentWorkspace.workspaceId); @@ -61,6 +61,7 @@ class UserWorkspaceBloc extends Bloc { workspaces: workspaces, isCollabWorkspaceOn: isCollabWorkspaceOn, actionResult: null, + members: members, ), ); }, @@ -198,6 +199,7 @@ class UserWorkspaceBloc extends Bloc { ), (e) => state.currentWorkspace, ); + final members = await _fetchMembers(currentWorkspace?.workspaceId); result ..onSuccess((s) { @@ -212,6 +214,7 @@ class UserWorkspaceBloc extends Bloc { emit( state.copyWith( currentWorkspace: currentWorkspace, + members: members, actionResult: UserWorkspaceActionResult( actionType: UserWorkspaceActionType.open, isLoading: false, @@ -415,6 +418,17 @@ class UserWorkspaceBloc extends Bloc { ..name = workspace.name ..createdAtTimestamp = workspace.createTime; } + + Future> _fetchMembers( + String? workspaceId, + ) async { + if (workspaceId == null) { + return []; + } + return _userService + .getWorkspaceMembers(workspaceId) + .fold((s) => s.items, (_) => []); + } } @freezed @@ -477,6 +491,7 @@ class UserWorkspaceState with _$UserWorkspaceState { @Default([]) List workspaces, @Default(null) UserWorkspaceActionResult? actionResult, @Default(false) bool isCollabWorkspaceOn, + @Default([]) List members, }) = _UserWorkspaceState; factory UserWorkspaceState.initial() => const UserWorkspaceState(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index c3b17cb742..c845b2ddcc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -41,7 +41,7 @@ extension ViewExtension on ViewPB { Widget defaultIcon() => FlowySvg( switch (layout) { ViewLayoutPB.Board => FlowySvgs.board_s, - ViewLayoutPB.Calendar => FlowySvgs.date_s, + ViewLayoutPB.Calendar => FlowySvgs.calendar_s, ViewLayoutPB.Grid => FlowySvgs.grid_s, ViewLayoutPB.Document => FlowySvgs.document_s, _ => FlowySvgs.document_s, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index 2087d1e476..d8c257e897 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; @@ -8,6 +6,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RecentViewsList extends StatelessWidget { @@ -24,7 +23,7 @@ class RecentViewsList extends StatelessWidget { builder: (context, state) { // We remove duplicates by converting the list to a set first final List recentViews = - state.views.reversed.toSet().toList(); + state.views.reversed.map((e) => e.item).toSet().toList(); return ListView.separated( shrinkWrap: true, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index bb7eb3600b..b35ae64ac5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -18,4 +18,10 @@ class HomeInsets { class HomeSpaceViewSizes { static const double leftPadding = 16.0; static const double viewHeight = 30.0; + + // mobile, m represents mobile + static const double mViewHeight = 48.0; + static const double mViewButtonDimension = 34.0; + static const double mHorizontalPadding = 20.0; + static const double mVerticalPadding = 12.0; } 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 16415e5e93..6ac2fd3e2e 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 @@ -82,7 +82,12 @@ class _FavoriteFolderState extends State { return []; } - return context.read().state.pinnedViews.map( + return context + .read() + .state + .pinnedViews + .map((e) => e.item) + .map( (view) => ViewItem( key: ValueKey( '${FolderSpaceType.favorite.name} ${view.id}', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart index 93bcccdf06..0e49c2acd6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart @@ -38,7 +38,9 @@ class SidebarFolder extends StatelessWidget { } return Padding( padding: const EdgeInsets.only(top: 16.0, bottom: 10), - child: FavoriteFolder(views: state.views), + child: FavoriteFolder( + views: state.views.map((e) => e.item).toList(), + ), ); }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart index e25c81b182..970ee82631 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart @@ -20,7 +20,7 @@ class ViewFavoriteButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final isFavorite = state.views.any((v) => v.id == view.id); + final isFavorite = state.views.any((v) => v.item.id == view.id); return Listener( onPointerDown: (_) => context.read().add(FavoriteEvent.toggle(view)), 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 5ad85efe31..357e565559 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; +import 'package:string_validator/string_validator.dart'; class UserAvatar extends StatelessWidget { const UserAvatar({ @@ -28,40 +29,79 @@ class UserAvatar extends StatelessWidget { @override Widget build(BuildContext context) { if (iconUrl.isEmpty) { - final String nameOrDefault = _userName(name); - final Color color = ColorGenerator(name).toColor(); - const initialsCount = 2; + return _buildEmptyAvatar(context); + } else if (isURL(iconUrl)) { + return _buildUrlAvatar(context); + } else { + return _buildEmojiAvatar(context); + } + } - // Taking the first letters of the name components and limiting to 2 elements - final nameInitials = nameOrDefault - .split(' ') - .where((element) => element.isNotEmpty) - .take(initialsCount) - .map((element) => element[0].toUpperCase()) - .join(); + Widget _buildEmptyAvatar(BuildContext context) { + final String nameOrDefault = _userName(name); + final Color color = ColorGenerator(name).toColor(); + const initialsCount = 2; - return Container( - width: size, - height: size, - alignment: Alignment.center, + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = nameOrDefault + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(); + + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, + ), + child: FlowyText.regular( + nameInitials, + color: Colors.black, + fontSize: fontSize, + ), + ); + } + + Widget _buildUrlAvatar(BuildContext context) { + return SizedBox.square( + dimension: size, + child: DecoratedBox( decoration: BoxDecoration( - color: color, shape: BoxShape.circle, border: isHovering ? Border.all( - color: _darken(color), + color: Theme.of(context).colorScheme.primary, width: 4, ) : null, ), - child: FlowyText.regular( - nameInitials, - color: Colors.black, - fontSize: fontSize, + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), + ), + ), ), - ); - } + ), + ); + } + Widget _buildEmojiAvatar(BuildContext context) { return SizedBox.square( dimension: size, child: DecoratedBox( 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 bcb4e13025..82779ecdae 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 @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; const String _emojiFontFamily = 'noto color emoji'; @@ -19,6 +20,7 @@ class FlowyText extends StatelessWidget { final double? lineHeight; final bool withTooltip; final StrutStyle? strutStyle; + final bool isEmoji; const FlowyText( this.text, { @@ -35,6 +37,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, this.strutStyle, }); @@ -51,6 +54,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, this.strutStyle, }) : fontWeight = FontWeight.w400, fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12; @@ -69,6 +73,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, this.strutStyle, }) : fontWeight = FontWeight.w400; @@ -86,6 +91,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, this.strutStyle, }) : fontWeight = FontWeight.w500; @@ -103,6 +109,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, this.strutStyle, }) : fontWeight = FontWeight.w600; @@ -120,15 +127,25 @@ class FlowyText extends StatelessWidget { this.lineHeight, this.withTooltip = false, this.strutStyle = const StrutStyle(forceStrutHeight: true), + this.isEmoji = true, + this.fontFamily, }) : fontWeight = FontWeight.w400, - fontFamily = _emojiFontFamily, fallbackFontFamily = null; @override Widget build(BuildContext context) { Widget child; - double fontSize = + var fontFamily = this.fontFamily; + var fallbackFontFamily = this.fallbackFontFamily; + if (isEmoji) { + 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; @@ -171,4 +188,12 @@ class FlowyText extends StatelessWidget { return child; } + + String? _loadEmojiFontFamilyIfNeeded() { + if (Platform.isLinux || Platform.isAndroid) { + return GoogleFonts.notoColorEmoji().fontFamily; + } + + return null; + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index 3d73a51d7b..5eb1ba066e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: loading_indicator: ^3.1.0 async: url_launcher: ^6.1.11 + google_fonts: ^6.1.0 # Federated Platform Interface flowy_infra_ui_platform_interface: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index bb49c0b84d..e4ddf13bfc 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -104,6 +104,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + auto_size_text_field: + dependency: "direct main" + description: + name: auto_size_text_field + sha256: c4ba8714ba4216ca122acac1573581dac499f3162c9218a28b573dca73721b3f + url: "https://pub.dev" + source: hosted + version: "2.2.3" avatar_stack: dependency: "direct main" description: @@ -1569,6 +1577,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + reorderable_tabbar: + dependency: "direct main" + description: + name: reorderable_tabbar + sha256: dd19d7b6f60f0dec4be02ba0a2c860f9acbe5a392cb8b5b8c1417cbfcbfe923f + url: "https://pub.dev" + source: hosted + version: "1.0.6" reorderables: dependency: "direct main" description: @@ -1992,6 +2008,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+1" + tab_indicator_styler: + dependency: transitive + description: + name: tab_indicator_styler + sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76" + url: "https://pub.dev" + source: hosted + version: "2.0.0" table_calendar: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0ffe803751..7c687baf54 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -136,6 +136,8 @@ dependencies: flutter_animate: ^4.5.0 permission_handler: ^11.3.1 scaled_app: ^2.3.0 + auto_size_text_field: ^2.2.3 + reorderable_tabbar: ^1.0.6 dev_dependencies: flutter_lints: ^3.0.1 diff --git a/frontend/resources/flowy_icons/16x/board.svg b/frontend/resources/flowy_icons/16x/board.svg index 550d045178..bde3149c31 100644 --- a/frontend/resources/flowy_icons/16x/board.svg +++ b/frontend/resources/flowy_icons/16x/board.svg @@ -1,6 +1,4 @@ - - - - - + + + diff --git a/frontend/resources/flowy_icons/16x/calendar.svg b/frontend/resources/flowy_icons/16x/calendar.svg new file mode 100644 index 0000000000..34f184b4d2 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/calendar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/document.svg b/frontend/resources/flowy_icons/16x/document.svg index abb59d5c14..d73738329c 100644 --- a/frontend/resources/flowy_icons/16x/document.svg +++ b/frontend/resources/flowy_icons/16x/document.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/resources/flowy_icons/16x/dot.svg b/frontend/resources/flowy_icons/16x/dot.svg new file mode 100644 index 0000000000..70d26f3988 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/grid.svg b/frontend/resources/flowy_icons/16x/grid.svg index 8164b24e6a..e1b7c1f148 100644 --- a/frontend/resources/flowy_icons/16x/grid.svg +++ b/frontend/resources/flowy_icons/16x/grid.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_collapse.svg b/frontend/resources/flowy_icons/16x/m_collapse.svg new file mode 100644 index 0000000000..c88452eafd --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_collapse.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/m_expand.svg b/frontend/resources/flowy_icons/16x/m_expand.svg new file mode 100644 index 0000000000..48c7bc5ebd --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_expand.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/m_space_add.svg b/frontend/resources/flowy_icons/16x/m_space_add.svg new file mode 100644 index 0000000000..dcab9d1eb5 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_space_add.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_space_more.svg b/frontend/resources/flowy_icons/16x/m_space_more.svg new file mode 100644 index 0000000000..ea557e960c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_space_more.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_spaces_expand.svg b/frontend/resources/flowy_icons/16x/m_spaces_expand.svg new file mode 100644 index 0000000000..af15050d18 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_spaces_expand.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/remove_from_recent.svg b/frontend/resources/flowy_icons/16x/remove_from_recent.svg new file mode 100644 index 0000000000..b12c8054aa --- /dev/null +++ b/frontend/resources/flowy_icons/16x/remove_from_recent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/success.svg b/frontend/resources/flowy_icons/16x/success.svg new file mode 100644 index 0000000000..771d8d7f5c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_home_add.svg b/frontend/resources/flowy_icons/24x/m_home_add.svg new file mode 100644 index 0000000000..bf93cf5e68 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_home_notification.svg b/frontend/resources/flowy_icons/24x/m_home_notification.svg new file mode 100644 index 0000000000..6d4b883701 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_notification.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_home_selected.svg b/frontend/resources/flowy_icons/24x/m_home_selected.svg new file mode 100644 index 0000000000..b92efcd453 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_selected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_home_unselected.svg b/frontend/resources/flowy_icons/24x/m_home_unselected.svg new file mode 100644 index 0000000000..7c34f3371f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_unselected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_setting.svg b/frontend/resources/flowy_icons/24x/m_setting.svg index d8be6a36b7..a32f3bc7b3 100644 --- a/frontend/resources/flowy_icons/24x/m_setting.svg +++ b/frontend/resources/flowy_icons/24x/m_setting.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/frontend/resources/flowy_icons/40x/m_empty_page.svg b/frontend/resources/flowy_icons/40x/m_empty_page.svg new file mode 100644 index 0000000000..c64507dd0a --- /dev/null +++ b/frontend/resources/flowy_icons/40x/m_empty_page.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b229e0f5b5..5b7de6a4d6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -245,7 +245,20 @@ "recent": "Recent", "today": "Today", "thisWeek": "This week", - "others": "Others" + "others": "Others", + "justNow": "just now", + "minutesAgo": "{count} minutes ago", + "lastViewed": "Last viewed", + "favoriteAt": "Favorited at", + "emptyRecent": "No Recent Documents", + "emptyRecentDescription": "As you view documents, they will appear here for easy retrieval", + "emptyFavorite": "No Favorite Documents", + "emptyFavoriteDescription": "Start exploring and mark documents as favorites. They’ll be listed here for quick access!", + "removePageFromRecent": "Remove this page from the Recent?", + "removeSuccess": "Removed successfully", + "favoriteSpace": "Favorites", + "RecentSpace": "Recent", + "Spaces": "Spaces" }, "notifications": { "export": { @@ -284,6 +297,7 @@ "update": "Update", "share": "Share", "removeFromFavorites": "Remove from favorites", + "removeFromRecent": "Remove from recent", "addToFavorites": "Add to favorites", "rename": "Rename", "helpCenter": "Help Center", diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index 0a2f34ca0a..fe55ad1bd2 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs @@ -158,7 +158,9 @@ impl FolderTest { assert_eq!(self.child_view, view, "View not equal"); }, FolderScript::ReadView(view_id) => { - let view = read_view(sdk, &view_id).await; + let mut view = read_view(sdk, &view_id).await; + // Ignore the last edited time + view.last_edited = 0; self.child_view = view; }, FolderScript::UpdateView { diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 466d30d06d..876b8a69f3 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -60,6 +60,18 @@ pub struct ViewPB { #[pb(index = 9, one_of)] pub extra: Option, + + // user_id + #[pb(index = 10, one_of)] + pub created_by: Option, + + // timestamp + #[pb(index = 11)] + pub last_edited: i64, + + // user_id + #[pb(index = 12, one_of)] + pub last_edited_by: Option, } pub fn view_pb_without_child_views(view: View) -> ViewPB { @@ -73,6 +85,9 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB { icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra, + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -87,6 +102,9 @@ pub fn view_pb_without_child_views_from_arc(view: Arc) -> ViewPB { icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra.clone(), + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -105,6 +123,9 @@ pub fn view_pb_with_child_views(view: Arc, child_views: Vec>) -> icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra.clone(), + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -155,11 +176,17 @@ pub struct RepeatedViewPB { #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] pub struct RepeatedFavoriteViewPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] -pub struct FavoriteViewPB { +pub struct RepeatedRecentViewPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct SectionViewPB { #[pb(index = 1)] pub item: ViewPB, #[pb(index = 2)] diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 06daab5a2f..888c26d2a6 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -284,7 +284,7 @@ pub(crate) async fn read_favorites_handler( let mut views = vec![]; for item in favorite_items { if let Ok(view) = folder.get_view_pb(&item.id).await { - views.push(FavoriteViewPB { + views.push(SectionViewPB { item: view, timestamp: item.timestamp, }); @@ -296,16 +296,19 @@ pub(crate) async fn read_favorites_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_recent_views_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; let recent_items = folder.get_my_recent_sections().await; let mut views = vec![]; for item in recent_items { if let Ok(view) = folder.get_view_pb(&item.id).await { - views.push(view); + views.push(SectionViewPB { + item: view, + timestamp: item.timestamp, + }); } } - data_result_ok(RepeatedViewPB { items: views }) + data_result_ok(RepeatedRecentViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 2901d19a63..febfc49b5e 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -155,7 +155,7 @@ pub enum FolderEvent { #[event(input = "UpdateViewIconPayloadPB")] UpdateViewIcon = 35, - #[event(output = "RepeatedViewPB")] + #[event(output = "RepeatedRecentViewPB")] ReadRecentViews = 36, // used for add or remove recent views, like history