menu.dart 8.3 KB

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