menu.dart 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. export './app/header/header.dart';
  2. export './app/menu_app.dart';
  3. import 'dart:io' show Platform;
  4. import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
  5. import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
  6. import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
  7. import 'package:flowy_infra/notifier.dart';
  8. import 'package:flowy_infra/size.dart';
  9. import 'package:flowy_infra/theme.dart';
  10. import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
  11. import 'package:flowy_infra_ui/widget/spacing.dart';
  12. import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile;
  13. import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
  14. import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
  15. import 'package:flutter/material.dart';
  16. import 'package:flutter_bloc/flutter_bloc.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/application/home/home_bloc.dart';
  23. import 'package:app_flowy/core/frameless_window.dart';
  24. // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
  25. import 'package:flowy_infra/image.dart';
  26. import 'package:flowy_infra_ui/style_widget/icon_button.dart';
  27. import 'app/menu_app.dart';
  28. import 'app/create_button.dart';
  29. import 'menu_user.dart';
  30. class HomeMenu extends StatelessWidget {
  31. final PublishNotifier<bool> _collapsedNotifier;
  32. final UserProfile user;
  33. final CurrentWorkspaceSetting workspaceSetting;
  34. const HomeMenu({
  35. Key? key,
  36. required this.user,
  37. required this.workspaceSetting,
  38. required PublishNotifier<bool> collapsedNotifier,
  39. }) : _collapsedNotifier = collapsedNotifier,
  40. super(key: key);
  41. @override
  42. Widget build(BuildContext context) {
  43. return MultiBlocProvider(
  44. providers: [
  45. BlocProvider<MenuBloc>(
  46. create: (context) {
  47. final menuBloc = getIt<MenuBloc>(param1: user, param2: workspaceSetting.workspace.id);
  48. menuBloc.add(const MenuEvent.initial());
  49. return menuBloc;
  50. },
  51. ),
  52. ],
  53. child: MultiBlocListener(
  54. listeners: [
  55. BlocListener<MenuBloc, MenuState>(
  56. listenWhen: (p, c) => p.plugin.id != c.plugin.id,
  57. listener: (context, state) {
  58. getIt<HomeStackManager>().setPlugin(state.plugin);
  59. },
  60. ),
  61. BlocListener<HomeBloc, HomeState>(
  62. listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
  63. listener: (context, state) {
  64. _collapsedNotifier.value = state.isMenuCollapsed;
  65. },
  66. )
  67. ],
  68. child: BlocBuilder<MenuBloc, MenuState>(
  69. builder: (context, state) => _renderBody(context),
  70. ),
  71. ),
  72. );
  73. }
  74. Widget _renderBody(BuildContext context) {
  75. // nested column: https://siddharthmolleti.com/flutter-box-constraints-nested-column-s-row-s-3dfacada7361
  76. final theme = context.watch<AppTheme>();
  77. return Container(
  78. color: theme.bg1,
  79. child: Column(
  80. mainAxisAlignment: MainAxisAlignment.start,
  81. children: [
  82. Expanded(
  83. child: Column(
  84. mainAxisAlignment: MainAxisAlignment.start,
  85. children: [
  86. const MenuTopBar(),
  87. const VSpace(10),
  88. _renderApps(context),
  89. ],
  90. ).padding(horizontal: Insets.l),
  91. ),
  92. const VSpace(20),
  93. const MenuTrash().padding(horizontal: Insets.l),
  94. const VSpace(20),
  95. _renderNewAppButton(context),
  96. ],
  97. ),
  98. );
  99. }
  100. Widget _renderApps(BuildContext context) {
  101. return ExpandableTheme(
  102. data: ExpandableThemeData(useInkWell: true, animationDuration: Durations.medium),
  103. child: Expanded(
  104. child: ScrollConfiguration(
  105. behavior: const ScrollBehavior().copyWith(scrollbars: false),
  106. child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
  107. selector: (state) => state.apps.map((app) => MenuApp(app, key: ValueKey(app.id))).toList(),
  108. builder: (context, menuItems) {
  109. return ReorderableListView.builder(
  110. itemCount: menuItems.length,
  111. buildDefaultDragHandles: false,
  112. header: Padding(
  113. padding: EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
  114. child: MenuUser(user),
  115. ),
  116. onReorder: (oldIndex, newIndex) {
  117. // Moving item1 from index 0 to index 1
  118. // expect: oldIndex: 0, newIndex: 1
  119. // receive: oldIndex: 0, newIndex: 2
  120. // Workaround: if newIndex > oldIndex, we just minus one
  121. int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
  122. context.read<MenuBloc>().add(MenuEvent.moveApp(oldIndex, index));
  123. },
  124. physics: StyledScrollPhysics(),
  125. itemBuilder: (BuildContext context, int index) {
  126. return ReorderableDragStartListener(
  127. key: ValueKey(menuItems[index].key),
  128. index: index,
  129. child: Padding(
  130. padding: EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2),
  131. child: menuItems[index],
  132. ),
  133. );
  134. },
  135. );
  136. },
  137. ),
  138. ),
  139. ),
  140. );
  141. }
  142. Widget _renderNewAppButton(BuildContext context) {
  143. return NewAppButton(
  144. press: (appName) => context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
  145. );
  146. }
  147. }
  148. class MenuSharedState {
  149. final ValueNotifier<View?> _latestOpenView = ValueNotifier<View?>(null);
  150. MenuSharedState({View? view}) {
  151. _latestOpenView.value = view;
  152. }
  153. View? get latestOpenView => _latestOpenView.value;
  154. set latestOpenView(View? view) {
  155. _latestOpenView.value = view;
  156. }
  157. VoidCallback addLatestViewListener(void Function(View?) callback) {
  158. listener() {
  159. callback(_latestOpenView.value);
  160. }
  161. _latestOpenView.addListener(listener);
  162. return listener;
  163. }
  164. void removeLatestViewListener(VoidCallback listener) {
  165. _latestOpenView.removeListener(listener);
  166. }
  167. }
  168. class MenuTopBar extends StatelessWidget {
  169. const MenuTopBar({Key? key}) : super(key: key);
  170. Widget renderIcon(BuildContext context) {
  171. if (Platform.isMacOS) {
  172. return Container();
  173. }
  174. final theme = context.watch<AppTheme>();
  175. return (theme.isDark
  176. ? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
  177. : svgWithSize("flowy_logo_with_text", const Size(92, 17)));
  178. }
  179. @override
  180. Widget build(BuildContext context) {
  181. final theme = context.watch<AppTheme>();
  182. return BlocBuilder<MenuBloc, MenuState>(
  183. builder: (context, state) {
  184. return SizedBox(
  185. height: HomeSizes.topBarHeight,
  186. child: MoveWindowDetector(
  187. child: Row(
  188. children: [
  189. renderIcon(context),
  190. const Spacer(),
  191. FlowyIconButton(
  192. width: 28,
  193. onPressed: () => context.read<HomeBloc>().add(const HomeEvent.collapseMenu()),
  194. iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
  195. icon: svgWidget("home/hide_menu", color: theme.iconColor),
  196. )
  197. ],
  198. )),
  199. );
  200. },
  201. );
  202. }
  203. }