menu.dart 9.6 KB

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