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 f9860598f8..f062bccffd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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'; @@ -18,12 +17,12 @@ class MobileFolders extends StatelessWidget { const MobileFolders({ super.key, required this.user, - required this.workspaceSetting, + required this.workspaceId, required this.showFavorite, }); final UserProfilePB user; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; final bool showFavorite; @override @@ -35,7 +34,7 @@ class MobileFolders extends StatelessWidget { ..add( SidebarSectionsEvent.initial( user, - workspaceSetting.workspaceId, + workspaceId, ), ), ), @@ -43,47 +42,57 @@ class MobileFolders extends StatelessWidget { create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), ), ], - child: BlocConsumer( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + child: BlocListener( listener: (context, state) { - final lastCreatedRootView = state.lastCreatedRootView; - if (lastCreatedRootView != null) { - context.pushView(lastCreatedRootView); - } - }, - builder: (context, state) { - final isCollaborativeWorkspace = - context.read().state.isCollabWorkspaceOn; - return SlidableAutoCloseBehavior( - child: Column( - children: [ - ...isCollaborativeWorkspace - ? [ - MobileSectionFolder( - title: LocaleKeys.sideBar_public.tr(), - categoryType: FolderCategoryType.public, - views: state.section.publicViews, - ), - const VSpace(8.0), - MobileSectionFolder( - title: LocaleKeys.sideBar_private.tr(), - categoryType: FolderCategoryType.private, - views: state.section.privateViews, - ), - ] - : [ - MobileSectionFolder( - title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, - views: state.section.publicViews, - ), - ], - const VSpace(8.0), - ], - ), - ); + context.read().add( + SidebarSectionsEvent.initial( + user, + state.currentWorkspace?.workspaceId ?? workspaceId, + ), + ); }, + child: BlocConsumer( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) { + final lastCreatedRootView = state.lastCreatedRootView; + if (lastCreatedRootView != null) { + context.pushView(lastCreatedRootView); + } + }, + builder: (context, state) { + final isCollaborativeWorkspace = + context.read().state.isCollabWorkspaceOn; + return SlidableAutoCloseBehavior( + child: Column( + children: [ + ...isCollaborativeWorkspace + ? [ + MobileSectionFolder( + title: LocaleKeys.sideBar_public.tr(), + categoryType: FolderCategoryType.public, + views: state.section.publicViews, + ), + const VSpace(8.0), + MobileSectionFolder( + title: LocaleKeys.sideBar_private.tr(), + categoryType: FolderCategoryType.private, + views: state.section.privateViews, + ), + ] + : [ + MobileSectionFolder( + title: LocaleKeys.sideBar_personal.tr(), + categoryType: FolderCategoryType.public, + views: state.section.publicViews, + ), + ], + const VSpace(8.0), + ], + ), + ); + }, + ), ), ); } 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 b56b36a839..bae27a8b5b 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 @@ -127,7 +127,9 @@ class MobileHomePage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 24), child: MobileFolders( user: userProfile, - workspaceSetting: workspaceSetting, + workspaceId: + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, showFavorite: false, ), ), 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 e8aa0d2b26..aa6eb4dc15 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; +import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; @@ -43,8 +45,9 @@ class MobileHomePageHeader extends StatelessWidget { : _MobileUser(userProfile: userProfile), ), IconButton( - onPressed: () => - context.push(MobileHomeSettingPage.routeName), + onPressed: () => context.push( + MobileHomeSettingPage.routeName, + ), icon: const FlowySvg(FlowySvgs.m_setting_m), ), ], @@ -108,25 +111,88 @@ class _MobileWorkspace extends StatelessWidget { if (currentWorkspace == null || workspaces.isEmpty) { return const SizedBox.shrink(); } - return Row( - children: [ - const HSpace(2.0), - SizedBox.square( - dimension: 34.0, - child: WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 26, - enableEdit: false, + return GestureDetector( + onTap: () { + _showSwitchWorkspacesBottomSheet( + context, + currentWorkspace, + workspaces, + ); + }, + child: Row( + children: [ + const HSpace(2.0), + SizedBox.square( + dimension: 34.0, + child: WorkspaceIcon( + workspace: currentWorkspace, + iconSize: 26, + enableEdit: false, + ), ), - ), - const HSpace(8), - Expanded( - child: FlowyText.medium( - currentWorkspace.name, - overflow: TextOverflow.ellipsis, + const HSpace(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FlowyText.medium( + currentWorkspace.name, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, + ), + const HSpace(4.0), + const FlowySvg(FlowySvgs.list_dropdown_s), + ], + ), + FlowyText.medium( + userProfile.email.isNotEmpty + ? userProfile.email + : userProfile.name, + overflow: TextOverflow.ellipsis, + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), ), - ), - ], + ], + ), + ); + }, + ); + } + + void _showSwitchWorkspacesBottomSheet( + BuildContext context, + UserWorkspacePB currentWorkspace, + List workspaces, + ) { + showMobileBottomSheet( + context, + showDivider: false, + showHeader: true, + showDragHandle: true, + title: LocaleKeys.workspace_menuTitle.tr(), + builder: (_) { + return MobileWorkspaceMenu( + userProfile: userProfile, + currentWorkspace: currentWorkspace, + workspaces: workspaces, + onWorkspaceSelected: (workspace) { + context.pop(); + + if (workspace == currentWorkspace) { + return; + } + + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + ), + ); + }, ); }, ); 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 95e4f6e11a..535271aadf 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 @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart'; import 'package:appflowy/workspace/application/recent/prelude.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_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'; @@ -22,31 +23,38 @@ class _MobileRecentFolderState extends State { ..add( const RecentViewsEvent.initial(), ), - child: BlocBuilder( - builder: (context, state) { - final ids = {}; - - List recentViews = state.views.reversed.toList(); - recentViews.retainWhere((element) => ids.add(element.id)); - - // only keep the first 20 items. - recentViews = recentViews.take(20).toList(); - - if (recentViews.isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - _RecentViews( - key: ValueKey(recentViews), - // the recent views are in reverse order - recentViews: recentViews, - ), - const VSpace(12.0), - ], - ); + child: BlocListener( + listener: (context, state) { + context.read().add( + const RecentViewsEvent.fetchRecentViews(), + ); }, + child: BlocBuilder( + builder: (context, state) { + final ids = {}; + + List recentViews = state.views.reversed.toList(); + recentViews.retainWhere((element) => ids.add(element.id)); + + // only keep the first 20 items. + recentViews = recentViews.take(20).toList(); + + if (recentViews.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + _RecentViews( + key: ValueKey(recentViews), + // the recent views are in reverse order + recentViews: recentViews, + ), + const VSpace(12.0), + ], + ); + }, + ), ), ); } 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 new file mode 100644 index 0000000000..d25bca8f83 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -0,0 +1,50 @@ +import 'package:appflowy/generated/flowy_svgs.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_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; + +// Only works on mobile. +class MobileWorkspaceMenu extends StatelessWidget { + const MobileWorkspaceMenu({ + super.key, + required this.userProfile, + required this.currentWorkspace, + required this.workspaces, + required this.onWorkspaceSelected, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB currentWorkspace; + final List workspaces; + final void Function(UserWorkspacePB workspace) onWorkspaceSelected; + + @override + Widget build(BuildContext context) { + final List children = []; + for (var i = 0; i < workspaces.length; i++) { + final workspace = workspaces[i]; + children.add( + FlowyOptionTile.text( + text: workspace.name, + 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), + ), + ); + } + return Column( + children: children, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index 66d25c58c6..6697db99c9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -1,10 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import '../widgets/widgets.dart'; @@ -32,6 +34,16 @@ class AboutSettingGroup extends StatelessWidget { ), onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'), ), + if (kDebugMode) + MobileSettingItem( + name: 'Feature Flags', + trailing: const Icon( + Icons.chevron_right, + ), + onTap: () { + context.push(FeatureFlagScreen.routeName); + }, + ), MobileSettingItem( name: LocaleKeys.settings_mobile_version.tr(), trailing: FlowyText( diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index f05b9b06ed..4bc9271e55 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -100,7 +100,7 @@ enum FeatureFlag { case FeatureFlag.membersSettings: return 'if it\'s on, you can see the members settings in the settings page'; case FeatureFlag.syncDocument: - return 'if it\'s on, the document will be synced the events from server in real-time'; + return 'if it\'s on, the document will be synced in real-time'; case FeatureFlag.unknown: return ''; } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index e9d7e13d2e..44f979a1ea 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -25,6 +25,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/time/duration.dart'; @@ -53,6 +54,7 @@ GoRouter generateRouter(Widget child) { _mobileHomeSettingPageRoute(), _mobileCloudSettingAppFlowyCloudPageRoute(), _mobileLaunchSettingsPageRoute(), + _mobileFeatureFlagPageRoute(), // view page _mobileEditorScreenRoute(), @@ -219,6 +221,16 @@ GoRoute _mobileLaunchSettingsPageRoute() { ); } +GoRoute _mobileFeatureFlagPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: FeatureFlagScreen.routeName, + pageBuilder: (context, state) { + return const MaterialExtendedPage(child: FeatureFlagScreen()); + }, + ); +} + GoRoute _mobileHomeTrashPageRoute() { return GoRoute( parentNavigatorKey: AppGlobals.rootNavKey, 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 ebe53420a5..c36a00aacf 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 @@ -28,14 +28,19 @@ class _WorkspaceIconState extends State { @override Widget build(BuildContext context) { - final child = widget.workspace.icon.isNotEmpty - ? FlowyText( - widget.workspace.icon, - textAlign: TextAlign.center, - fontSize: widget.iconSize, + Widget child = widget.workspace.icon.isNotEmpty + ? Container( + width: widget.iconSize, + margin: const EdgeInsets.all(2), + child: FlowyText( + widget.workspace.icon, + textAlign: TextAlign.center, + fontSize: widget.iconSize, + ), ) : Container( alignment: Alignment.center, + width: widget.iconSize, decoration: BoxDecoration( color: ColorGenerator.generateColorFromString( widget.workspace.name, @@ -51,29 +56,32 @@ class _WorkspaceIconState extends State { color: Colors.black, ), ); - return AppFlowyPopover( - offset: const Offset(0, 8), - controller: controller, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (BuildContext popoverContext) { - return FlowyIconPicker( - onSelected: (result) { - context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - widget.workspace.workspaceId, - result.emoji, - ), - ); - controller.close(); - }, - ); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - ); + if (widget.enableEdit) { + child = AppFlowyPopover( + offset: const Offset(0, 8), + controller: controller, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (result) { + context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + widget.workspace.workspaceId, + result.emoji, + ), + ); + controller.close(); + }, + ); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + ); + } + return child; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart index 772857433e..9865372105 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -59,7 +59,7 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { widget.featureFlag.description, maxLines: 3, ), - trailing: Switch( + trailing: Switch.adaptive( value: widget.featureFlag.isOn, onChanged: (value) { setState(() { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart new file mode 100644 index 0000000000..c55522b7d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart @@ -0,0 +1,20 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; +import 'package:flutter/material.dart'; + +class FeatureFlagScreen extends StatelessWidget { + const FeatureFlagScreen({ + super.key, + }); + + static const routeName = '/feature_flag'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Feature Flags'), + ), + body: const FeatureFlagsPage(), + ); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 860fbb64ac..f16225a1a1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -65,6 +65,7 @@ "exportLogFiles": "Export log files", "reachOut": "Reach out on Discord" }, + "menuTitle": "Workspaces", "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", "createSuccess": "Workspace created successfully", "createFailed": "Failed to create workspace",