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 ca3e83662e..9d8cbf608e 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 @@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_fa import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +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'; @@ -16,11 +16,11 @@ class MobileFavoritePageFolder extends StatelessWidget { const MobileFavoritePageFolder({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceId, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; @override Widget build(BuildContext context) { @@ -31,7 +31,7 @@ class MobileFavoritePageFolder extends StatelessWidget { ..add( SidebarSectionsEvent.initial( userProfile, - workspaceSetting.workspaceId, + workspaceId, ), ), ), @@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget { create: (_) => FavoriteBloc()..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.views.isEmpty) { - return FlowyMobileStateContainer.info( - emoji: '😁', - title: LocaleKeys.favorite_noFavorite.tr(), - description: LocaleKeys.favorite_noFavoriteHintText.tr(), + child: BlocListener( + listener: (context, state) { + context.read().add( + const FavoriteEvent.initial(), ); - } - return Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SlidableAutoCloseBehavior( - child: Column( - children: [ - MobileFavoriteFolder( - showHeader: false, - forceExpanded: true, - views: favoriteState.views, - ), - const VSpace(100.0), - ], + }, + 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.views.isEmpty) { + return FlowyMobileStateContainer.info( + emoji: '😁', + title: LocaleKeys.favorite_noFavorite.tr(), + description: LocaleKeys.favorite_noFavoriteHintText.tr(), + ); + } + return Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SlidableAutoCloseBehavior( + child: Column( + children: [ + MobileFavoriteFolder( + showHeader: false, + forceExpanded: true, + views: favoriteState.views, + ), + 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 934de55a60..7afc740b45 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 @@ -57,9 +57,17 @@ class MobileFavoriteScreen extends StatelessWidget { ..add( const UserWorkspaceEvent.initial(), ), - child: MobileFavoritePage( - userProfile: userProfile, - workspaceSetting: workspaceSetting, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + return MobileFavoritePage( + userProfile: userProfile, + workspaceId: state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ); + }, ), ), ), @@ -73,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget { const MobileFavoritePage({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceId, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; @override Widget build(BuildContext context) { @@ -100,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget { Expanded( child: MobileFavoritePageFolder( userProfile: userProfile, - workspaceSetting: workspaceSetting, + workspaceId: workspaceId, ), ), ], 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 61ea86cebf..58e34c63d1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,10 +1,13 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_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'; @@ -38,9 +41,19 @@ class MobileSectionFolder extends StatelessWidget { onPressed: () => context .read() .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context.read().add( - const FolderEvent.expandOrUnExpand(isExpanded: true), - ), + onAdded: () { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + index: 0, + viewSection: categoryType.toViewSectionPB, + ), + ); + context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ); + }, ), const VSpace(8.0), const Divider( 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 4ead7a2851..3ba15df25d 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,11 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.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'; @visibleForTesting const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey'); @@ -72,15 +67,7 @@ class _MobileSectionFolderHeaderState extends State { FlowySvgs.add_s, size: Size.square(iconSize), ), - onPressed: () { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - index: 0, - viewSection: ViewSectionPB.Public, - ), - ); - }, + onPressed: widget.onAdded, ), ], ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index d25bca8f83..a41e8aba97 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -1,8 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; // Only works on mobile. class MobileWorkspaceMenu extends StatelessWidget { @@ -25,21 +30,12 @@ class MobileWorkspaceMenu extends StatelessWidget { for (var i = 0; i < workspaces.length; i++) { final workspace = workspaces[i]; children.add( - FlowyOptionTile.text( - text: workspace.name, + _WorkspaceMenuItem( + userProfile: userProfile, + workspace: workspace, showTopBorder: i == 0, - leftIcon: WorkspaceIcon( - enableEdit: false, - iconSize: 22, - workspace: workspace, - ), - trailing: workspace.workspaceId == currentWorkspace.workspaceId - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - blendMode: null, - ) - : null, - onTap: () => onWorkspaceSelected(workspace), + currentWorkspace: currentWorkspace, + onWorkspaceSelected: onWorkspaceSelected, ), ); } @@ -48,3 +44,76 @@ class MobileWorkspaceMenu extends StatelessWidget { ); } } + +class _WorkspaceMenuItem extends StatelessWidget { + const _WorkspaceMenuItem({ + required this.userProfile, + required this.workspace, + required this.showTopBorder, + required this.currentWorkspace, + required this.onWorkspaceSelected, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB workspace; + final bool showTopBorder; + final UserWorkspacePB currentWorkspace; + final void Function(UserWorkspacePB workspace) onWorkspaceSelected; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => WorkspaceMemberBloc( + userProfile: userProfile, + workspace: workspace, + )..add(const WorkspaceMemberEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final members = state.members; + return FlowyOptionTile.text( + content: Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText( + workspace.name, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + FlowyText( + state.isLoading + ? '' + : LocaleKeys.settings_appearance_members_membersCount + .plural( + members.length, + ), + fontSize: 10.0, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + ), + height: 60, + showTopBorder: showTopBorder, + leftIcon: WorkspaceIcon( + enableEdit: false, + iconSize: 26, + workspace: workspace, + ), + trailing: workspace.workspaceId == currentWorkspace.workspaceId + ? const FlowySvg( + FlowySvgs.m_blue_check_s, + blendMode: null, + ) + : null, + onTap: () => onWorkspaceSelected(workspace), + ); + }, + ), + ); + } +} 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 c1ffc78e76..44eba47bcb 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 @@ -406,7 +406,10 @@ class _SingleMobileInnerViewItemState extends State { ViewEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, - section: widget.categoryType.toViewSectionPB, + section: + widget.categoryType != FolderCategoryType.favorite + ? widget.categoryType.toViewSectionPB + : null, ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index ceca40d019..4f76003e23 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget { this.content, this.backgroundColor, this.fontFamily, + this.height, }); factory FlowyOptionTile.text({ - required String text, + String? text, + Widget? content, Color? textColor, bool showTopBorder = true, bool showBottomBorder = true, Widget? leftIcon, Widget? trailing, VoidCallback? onTap, + double? height, }) { return FlowyOptionTile._( type: FlowyOptionTileType.text, text: text, + content: content, textColor: textColor, onTap: onTap, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, leading: leftIcon, trailing: trailing, + height: height, ); } @@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget { final Color? backgroundColor; final String? fontFamily; + final double? height; + @override Widget build(BuildContext context) { final leadingWidget = _buildLeading(); @@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget { color: backgroundColor, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - if (leadingWidget != null) leadingWidget, - if (content != null) content!, - if (content == null) _buildText(), - if (content == null) _buildTextField(), - if (trailing != null) trailing!, - ], + child: SizedBox( + height: height, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + if (leadingWidget != null) leadingWidget, + if (content != null) content!, + if (content == null) _buildText(), + if (content == null) _buildTextField(), + if (trailing != null) trailing!, + ], + ), ), ), ); 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 1f88340cc1..7b9e86e16b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -27,8 +27,8 @@ class FavoriteBloc extends Bloc { void _dispatch() { on( (event, emit) async { - await event.map( - initial: (e) async { + await event.when( + initial: () async { _listener.start( favoritesUpdated: _onFavoritesUpdated, ); @@ -44,23 +44,23 @@ class FavoriteBloc extends Bloc { ), ); }, - didFavorite: (e) { + fetchFavorites: () async { + final result = await _service.readFavorites(); emit( - state.copyWith(views: [...state.views, ...e.favorite.items]), + result.fold( + (view) => state.copyWith( + views: view.items, + ), + (error) => state.copyWith( + views: [], + ), + ), ); }, - didUnfavorite: (e) { - final views = [...state.views]..removeWhere( - (view) => e.favorite.items.any((item) => item.id == view.id), - ); - emit( - state.copyWith(views: views), - ); - }, - toggle: (e) async { + toggle: (view) async { await _service.toggleFavorite( - e.view.id, - !e.view.isFavorite, + view.id, + !view.isFavorite, ); }, ); @@ -73,9 +73,7 @@ class FavoriteBloc extends Bloc { bool didFavorite, ) { favoriteOrFailed.fold( - (favorite) => didFavorite - ? add(FavoriteEvent.didFavorite(favorite)) - : add(FavoriteEvent.didUnfavorite(favorite)), + (favorite) => add(const FetchFavorites()), (error) => Log.error(error), ); } @@ -84,11 +82,8 @@ class FavoriteBloc extends Bloc { @freezed class FavoriteEvent with _$FavoriteEvent { const factory FavoriteEvent.initial() = Initial; - const factory FavoriteEvent.didFavorite(RepeatedViewPB favorite) = - DidFavorite; - const factory FavoriteEvent.didUnfavorite(RepeatedViewPB favorite) = - DidUnfavorite; const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; + const factory FavoriteEvent.fetchFavorites() = FetchFavorites; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart index 6606ab26eb..0cadf9c91f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart @@ -37,24 +37,25 @@ class FavoriteListener { FolderNotification ty, FlowyResult result, ) { - if (_favoriteUpdated == null) { - return; - } - - final isFavorite = ty == FolderNotification.DidFavoriteView; - result.fold( - (payload) { - final view = RepeatedViewPB.fromBuffer(payload); - _favoriteUpdated!( - FlowyResult.success(view), - isFavorite, + switch (ty) { + case FolderNotification.DidFavoriteView: + result.onSuccess( + (success) => _favoriteUpdated?.call( + FlowyResult.success(RepeatedViewPB.fromBuffer(success)), + true, + ), ); - }, - (error) => _favoriteUpdated!( - FlowyResult.failure(error), - isFavorite, - ), - ); + case FolderNotification.DidUnfavoriteView: + result.map( + (success) => _favoriteUpdated?.call( + FlowyResult.success(RepeatedViewPB.fromBuffer(success)), + false, + ), + ); + break; + default: + break; + } } Future stop() async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 8cfa6a2014..3a960fb114 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -363,7 +363,7 @@ class ViewEvent with _$ViewEvent { ViewLayoutPB layoutType, { /// open the view after created @Default(true) bool openAfterCreated, - required ViewSectionPB section, + ViewSectionPB? section, }) = CreateView; const factory ViewEvent.viewDidUpdate( FlowyResult result, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart index f75dcb91db..bb8ab29781 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -174,10 +174,10 @@ class _SidebarSwitchWorkspaceButtonState children: [ const HSpace(2.0), SizedBox.square( - dimension: 28.0, + dimension: 30.0, child: WorkspaceIcon( workspace: widget.currentWorkspace, - iconSize: 18, + iconSize: 20, enableEdit: false, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index c36a00aacf..ee0eb69a1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; @@ -31,16 +33,16 @@ class _WorkspaceIconState extends State { Widget child = widget.workspace.icon.isNotEmpty ? Container( width: widget.iconSize, - margin: const EdgeInsets.all(2), + alignment: Alignment.center, child: FlowyText( widget.workspace.icon, - textAlign: TextAlign.center, fontSize: widget.iconSize, ), ) : Container( alignment: Alignment.center, width: widget.iconSize, + height: max(widget.iconSize, 26), decoration: BoxDecoration( color: ColorGenerator.generateColorFromString( widget.workspace.name,