menu.dart 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. export './app/header/header.dart';
  2. export './app/menu_app.dart';
  3. import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
  4. import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
  5. import 'package:flowy_infra/notifier.dart';
  6. import 'package:flowy_infra/size.dart';
  7. import 'package:flowy_infra/theme.dart';
  8. import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
  9. import 'package:flowy_infra_ui/widget/spacing.dart';
  10. import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile;
  11. import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
  12. import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
  13. import 'package:flutter/material.dart';
  14. import 'package:flutter_bloc/flutter_bloc.dart';
  15. import 'package:provider/provider.dart';
  16. import 'package:styled_widget/styled_widget.dart';
  17. import 'package:expandable/expandable.dart';
  18. import 'package:flowy_infra/time/duration.dart';
  19. import 'package:app_flowy/startup/startup.dart';
  20. import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
  21. import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
  22. import 'package:flowy_infra/image.dart';
  23. import 'package:flowy_infra_ui/style_widget/icon_button.dart';
  24. import 'app/menu_app.dart';
  25. import 'app/create_button.dart';
  26. import 'menu_user.dart';
  27. class HomeMenu extends StatefulWidget {
  28. final PublishNotifier<bool> _collapsedNotifier;
  29. final UserProfile user;
  30. final CurrentWorkspaceSetting workspaceSetting;
  31. const HomeMenu({
  32. Key? key,
  33. required this.user,
  34. required this.workspaceSetting,
  35. required PublishNotifier<bool> collapsedNotifier,
  36. }) : _collapsedNotifier = collapsedNotifier,
  37. super(key: key);
  38. @override
  39. State<HomeMenu> createState() => _HomeMenuState();
  40. }
  41. class _HomeMenuState extends State<HomeMenu> {
  42. /// Maps the hashmap of the menu items to their index in reorderable list view.
  43. //TODO @gaganyadav80: Retain this map to persist on app restarts.
  44. final Map<int, int> _menuItemIndex = <int, int>{};
  45. @override
  46. Widget build(BuildContext context) {
  47. return MultiBlocProvider(
  48. providers: [
  49. BlocProvider<MenuBloc>(
  50. create: (context) {
  51. final menuBloc = getIt<MenuBloc>(param1: widget.user, param2: widget.workspaceSetting.workspace.id);
  52. menuBloc.add(const MenuEvent.initial());
  53. return menuBloc;
  54. },
  55. ),
  56. ],
  57. child: MultiBlocListener(
  58. listeners: [
  59. BlocListener<MenuBloc, MenuState>(
  60. listenWhen: (p, c) => p.plugin.id != c.plugin.id,
  61. listener: (context, state) {
  62. getIt<HomeStackManager>().setPlugin(state.plugin);
  63. },
  64. ),
  65. BlocListener<MenuBloc, MenuState>(
  66. listenWhen: (p, c) => p.isCollapse != c.isCollapse,
  67. listener: (context, state) {
  68. widget._collapsedNotifier.value = state.isCollapse;
  69. },
  70. )
  71. ],
  72. child: BlocBuilder<MenuBloc, MenuState>(
  73. builder: (context, state) => _renderBody(context),
  74. ),
  75. ),
  76. );
  77. }
  78. Widget _renderBody(BuildContext context) {
  79. // nested column: https://siddharthmolleti.com/flutter-box-constraints-nested-column-s-row-s-3dfacada7361
  80. final theme = context.watch<AppTheme>();
  81. return Container(
  82. color: theme.bg1,
  83. child: Column(
  84. mainAxisAlignment: MainAxisAlignment.start,
  85. children: [
  86. Expanded(
  87. child: Column(
  88. mainAxisAlignment: MainAxisAlignment.start,
  89. children: [
  90. const MenuTopBar(),
  91. const VSpace(10),
  92. _renderApps(context),
  93. ],
  94. ).padding(horizontal: Insets.l),
  95. ),
  96. const VSpace(20),
  97. _renderTrash(context).padding(horizontal: Insets.l),
  98. const VSpace(20),
  99. _renderNewAppButton(context),
  100. ],
  101. ),
  102. );
  103. }
  104. Widget _renderApps(BuildContext context) {
  105. return ExpandableTheme(
  106. data: ExpandableThemeData(useInkWell: true, animationDuration: Durations.medium),
  107. child: Expanded(
  108. child: ScrollConfiguration(
  109. behavior: const ScrollBehavior().copyWith(scrollbars: false),
  110. child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
  111. selector: (state) {
  112. List<Widget> menuItems = [];
  113. // menuItems.add(MenuUser(user));
  114. List<MenuApp> appWidgets =
  115. state.apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList());
  116. // menuItems.addAll(appWidgets);
  117. for (int i = 0; i < appWidgets.length; i++) {
  118. if (_menuItemIndex[appWidgets[i].key.hashCode] == null) {
  119. _menuItemIndex[appWidgets[i].key.hashCode] = i;
  120. }
  121. menuItems.insert(_menuItemIndex[appWidgets[i].key.hashCode]!, appWidgets[i]);
  122. }
  123. return menuItems;
  124. },
  125. builder: (context, menuItems) {
  126. return ReorderableListView.builder(
  127. itemCount: menuItems.length,
  128. buildDefaultDragHandles: false,
  129. header: Padding(
  130. padding: EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
  131. child: MenuUser(widget.user),
  132. ),
  133. onReorder: (oldIndex, newIndex) {
  134. int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
  135. Widget menu = menuItems.removeAt(oldIndex);
  136. menuItems.insert(index, menu);
  137. final menuBloc = context.read<MenuBloc>();
  138. menuBloc.state.apps.forEach((a) {
  139. var app = a.removeAt(oldIndex);
  140. a.insert(index, app);
  141. });
  142. _menuItemIndex[menu.key.hashCode] = index;
  143. },
  144. physics: StyledScrollPhysics(),
  145. itemBuilder: (BuildContext context, int index) {
  146. //? @gaganyadav80: To mimic the ListView.separated behavior, we need to add a padding.
  147. // EdgeInsets padding = EdgeInsets.zero;
  148. // if (index == 0) {
  149. // padding = EdgeInsets.only(bottom: MenuAppSizes.appVPadding / 2);
  150. // } else if (index == menuItems.length - 1) {
  151. // padding = EdgeInsets.only(top: MenuAppSizes.appVPadding / 2);
  152. // } else {
  153. // padding = EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2);
  154. // }
  155. return ReorderableDragStartListener(
  156. key: ValueKey(menuItems[index].hashCode),
  157. index: index,
  158. child: Padding(
  159. padding: EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2),
  160. child: menuItems[index],
  161. ),
  162. );
  163. },
  164. );
  165. },
  166. ),
  167. ),
  168. ),
  169. );
  170. }
  171. Widget _renderTrash(BuildContext context) {
  172. return const MenuTrash();
  173. }
  174. Widget _renderNewAppButton(BuildContext context) {
  175. return NewAppButton(
  176. press: (appName) => context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
  177. );
  178. }
  179. }
  180. class MenuSharedState {
  181. final ValueNotifier<View?> _latestOpenView = ValueNotifier<View?>(null);
  182. MenuSharedState({View? view}) {
  183. if (view != null) {
  184. _latestOpenView.value = view;
  185. }
  186. }
  187. View? get latestOpenView => _latestOpenView.value;
  188. set latestOpenView(View? view) {
  189. _latestOpenView.value = view;
  190. }
  191. VoidCallback addLatestViewListener(void Function(View?) latestViewDidChange) {
  192. onChanged() {
  193. latestViewDidChange(_latestOpenView.value);
  194. }
  195. _latestOpenView.addListener(onChanged);
  196. return onChanged;
  197. }
  198. void removeLatestViewListener(VoidCallback fn) {
  199. _latestOpenView.removeListener(fn);
  200. }
  201. }
  202. class MenuTopBar extends StatelessWidget {
  203. const MenuTopBar({Key? key}) : super(key: key);
  204. @override
  205. Widget build(BuildContext context) {
  206. final theme = context.watch<AppTheme>();
  207. return BlocBuilder<MenuBloc, MenuState>(
  208. builder: (context, state) {
  209. return SizedBox(
  210. height: HomeSizes.topBarHeight,
  211. child: Row(
  212. children: [
  213. (theme.isDark
  214. ? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
  215. : svgWithSize("flowy_logo_with_text", const Size(92, 17))),
  216. const Spacer(),
  217. FlowyIconButton(
  218. width: 28,
  219. onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
  220. iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
  221. icon: svgWidget("home/hide_menu", color: theme.iconColor),
  222. )
  223. ],
  224. ),
  225. );
  226. },
  227. );
  228. }
  229. }