feat: support for switching workspace on mobile (#4990)

* feat: support switching workspaces on mobile

* fix: sync recent section
This commit is contained in:
Lucas.Xu 2024-03-26 10:21:49 +07:00 committed by GitHub
parent b8e3de97a5
commit a1b183f330
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 308 additions and 120 deletions

View File

@ -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<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
final lastCreatedRootView = state.lastCreatedRootView;
if (lastCreatedRootView != null) {
context.pushView(lastCreatedRootView);
}
},
builder: (context, state) {
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().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<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial(
user,
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
},
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
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<UserWorkspaceBloc>().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),
],
),
);
},
),
),
);
}

View File

@ -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,
),
),

View File

@ -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<UserWorkspacePB> 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<UserWorkspaceBloc>().add(
UserWorkspaceEvent.openWorkspace(
workspace.workspaceId,
),
);
},
);
},
);

View File

@ -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<MobileRecentFolder> {
..add(
const RecentViewsEvent.initial(),
),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final ids = <String>{};
List<ViewPB> 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<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<RecentViewsBloc>().add(
const RecentViewsEvent.fetchRecentViews(),
);
},
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final ids = <String>{};
List<ViewPB> 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),
],
);
},
),
),
);
}

View File

@ -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<UserWorkspacePB> workspaces;
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
@override
Widget build(BuildContext context) {
final List<Widget> 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,
);
}
}

View File

@ -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(

View File

@ -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 '';
}

View File

@ -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,

View File

@ -28,14 +28,19 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
@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<WorkspaceIcon> {
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<UserWorkspaceBloc>().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<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
widget.workspace.workspaceId,
result.emoji,
),
);
controller.close();
},
);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
);
}
return child;
}
}

View File

@ -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(() {

View File

@ -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(),
);
}
}

View File

@ -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",