mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support switching space on mobile (#5527)
This commit is contained in:
parent
2d674060c6
commit
dc12938ab6
@ -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 {
|
||||
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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()),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
}
|
@ -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(),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -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 =
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user