feat: support switching space on mobile (#5527)

This commit is contained in:
Lucas.Xu 2024-06-13 14:14:18 +08:00 committed by GitHub
parent 2d674060c6
commit dc12938ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 484 additions and 44 deletions

View File

@ -3,6 +3,7 @@ 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/mobile/presentation/home/space/mobile_space.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/sidebar/folder/folder_bloc.dart';
@ -53,53 +54,88 @@ class MobileFolders extends StatelessWidget {
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.reset(
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_workspace.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
spaceType: FolderSpaceType.private,
views: state.section.privateViews,
),
]
: [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
],
const VSpace(4.0),
const _TrashButton(),
],
),
);
},
child: MultiBlocListener(
listeners: [
BlocListener<SpaceBloc, SpaceState>(
listenWhen: (p, c) =>
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
listener: (context, state) {
final lastCreatedPage = state.lastCreatedPage;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
}
},
),
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) {
final lastCreatedPage = state.lastCreatedRootView;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
}
},
),
],
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) {
return SlidableAutoCloseBehavior(
child: Column(
children: [
..._buildSpaceOrSection(context, state),
const VSpace(4.0),
const _TrashButton(),
],
),
);
},
),
),
),
);
}
List<Widget> _buildSpaceOrSection(
BuildContext context,
SidebarSectionsState state,
) {
if (context.watch<SpaceBloc>().state.spaces.isNotEmpty) {
return [
const MobileSpace(),
];
}
if (context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn) {
return [
MobileSectionFolder(
title: LocaleKeys.sideBar_workspace.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
spaceType: FolderSpaceType.private,
views: state.section.privateViews,
),
];
}
return [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
];
}
}
class _TrashButton extends StatelessWidget {

View File

@ -0,0 +1,143 @@
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/home/space/mobile_space_header.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_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_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSpace extends StatefulWidget {
const MobileSpace({super.key});
@override
State<MobileSpace> createState() => _MobileSpaceState();
}
class _MobileSpaceState extends State<MobileSpace> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
if (state.spaces.isEmpty) {
return const SizedBox.shrink();
}
final currentSpace = state.currentSpace ?? state.spaces.first;
return Column(
children: [
MobileSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
onAdded: () {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: currentSpace.spacePermission ==
SpacePermission.publicToAll
? ViewSectionPB.Public
: ViewSectionPB.Private,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(currentSpace, true),
);
},
onPressed: () => _showSpaceMenu(context),
),
_Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
),
],
);
},
);
}
void _showSpaceMenu(BuildContext context) {
showMobileBottomSheet(
context,
showDivider: false,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
showDoneButton: true,
title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
return BlocProvider.value(
value: context.read<SpaceBloc>(),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: MobileSpaceMenu(),
),
);
},
);
}
}
class _Pages extends StatelessWidget {
const _Pages({
super.key,
required this.space,
});
final ViewPB space;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ViewBloc(view: space)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final spaceType = space.spacePermission == SpacePermission.publicToAll
? FolderSpaceType.public
: FolderSpaceType.private;
return Column(
children: state.view.childViews
.map(
(view) => MobileViewItem(
key: ValueKey(
'${space.id} ${view.id}',
),
spaceType: spaceType,
isFirstChild: view.id == state.view.childViews.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
spaceType: spaceType,
needSpace: false,
);
},
),
)
.toList(),
);
},
),
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
class MobileSpaceHeader extends StatelessWidget {
const MobileSpaceHeader({
super.key,
required this.space,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
final ViewPB space;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onPressed,
child: SizedBox(
height: 48,
child: Row(
children: [
SpaceIcon(
dimension: 24,
space: space,
cornerRadius: 6.0,
),
const HSpace(8),
FlowyText.medium(
space.name,
lineHeight: 1.15,
fontSize: 16.0,
),
const HSpace(4.0),
const FlowySvg(
FlowySvgs.workspace_drop_down_menu_show_s,
),
const Spacer(),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onAdded,
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
),
],
),
),
);
}
// Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {
// switch (type) {
// case SpaceMoreActionType.rename:
// await _showRenameDialog();
// break;
// case SpaceMoreActionType.changeIcon:
// final (String icon, String iconColor) = data;
// context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));
// break;
// case SpaceMoreActionType.manage:
// _showManageSpaceDialog(context);
// break;
// case SpaceMoreActionType.addNewSpace:
// break;
// case SpaceMoreActionType.collapseAllPages:
// break;
// case SpaceMoreActionType.delete:
// _showDeleteSpaceDialog(context);
// break;
// case SpaceMoreActionType.divider:
// break;
// }
// }
// Future<void> _showRenameDialog() async {
// await NavigatorTextFieldDialog(
// title: LocaleKeys.space_rename.tr(),
// value: space.name,
// autoSelectAllText: true,
// onConfirm: (name, _) {
// context.read<SpaceBloc>().add(SpaceEvent.rename(space, name));
// },
// ).show(context);
// }
// void _showManageSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const ManageSpacePopup(),
// ),
// );
// },
// );
// }
// void _showDeleteSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const SizedBox(width: 440, child: DeleteSpacePopup()),
// ),
// );
// },
// );
// }
}

View File

@ -0,0 +1,128 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.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 MobileSpaceMenu extends StatelessWidget {
const MobileSpaceMenu({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(4.0),
for (final space in state.spaces)
SizedBox(
height: 52,
child: _SidebarSpaceMenuItem(
space: space,
isSelected: state.currentSpace?.id == space.id,
),
),
// const Padding(
// padding: EdgeInsets.symmetric(vertical: 8.0),
// child: Divider(
// height: 0.5,
// ),
// ),
// const SizedBox(
// height: 52,
// child: _CreateSpaceButton(),
// ),
],
);
},
);
}
}
class _SidebarSpaceMenuItem extends StatelessWidget {
const _SidebarSpaceMenuItem({
required this.space,
required this.isSelected,
});
final ViewPB space;
final bool isSelected;
@override
Widget build(BuildContext context) {
return FlowyButton(
text: Row(
children: [
FlowyText.medium(
space.name,
),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
const FlowySvg(
FlowySvgs.space_lock_s,
size: Size.square(12),
),
],
),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 24,
space: space,
cornerRadius: 6.0,
),
leftIconSize: const Size.square(20),
rightIcon: isSelected
? const FlowySvg(
FlowySvgs.workspace_selected_s,
blendMode: null,
)
: null,
onTap: () {
context.read<SpaceBloc>().add(SpaceEvent.open(space));
Navigator.of(context).pop();
},
);
}
}
// class _CreateSpaceButton extends StatelessWidget {
// const _CreateSpaceButton();
// @override
// Widget build(BuildContext context) {
// return FlowyButton(
// text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),
// iconPadding: 10,
// leftIcon: const FlowySvg(
// FlowySvgs.space_add_s,
// ),
// leftIconSize: const Size.square(20),
// onTap: () {
// PopoverContainer.of(context).close();
// _showCreateSpaceDialog(context);
// },
// );
// }
// void _showCreateSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const CreateSpacePopup(),
// ),
// );
// },
// );
// }
// }

View File

@ -332,7 +332,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
final spaceId =
await getIt<KeyValueStorage>().get(KVKeys.lastOpenedSpaceId);
if (spaceId == null) {
return null;
return spaces.first;
}
final space =

View File

@ -1911,7 +1911,8 @@
"dangerZone": "Danger Zone",
"unableToDeleteLastSpace": "Cannot delete the last space",
"unableToDeleteSpaceNotCreatedByYou": "Cannot delete a space created by others",
"enableSpacesForYourWorkspace": "Enable spaces for your workspace"
"enableSpacesForYourWorkspace": "Enable spaces for your workspace",
"title": "Spaces"
}
}