menu.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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/application/home/home_setting_bloc.dart';
  7. import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
  8. import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
  9. import 'package:easy_localization/easy_localization.dart';
  10. import 'package:flowy_infra/notifier.dart';
  11. import 'package:flowy_infra/size.dart';
  12. import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
  13. import 'package:flowy_infra_ui/widget/spacing.dart';
  14. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
  15. show UserProfilePB;
  16. import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
  17. import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
  18. import 'package:flutter/material.dart';
  19. import 'package:flutter_bloc/flutter_bloc.dart';
  20. import 'package:styled_widget/styled_widget.dart';
  21. import 'package:expandable/expandable.dart';
  22. import 'package:flowy_infra/time/duration.dart';
  23. import 'package:app_flowy/startup/startup.dart';
  24. import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
  25. import 'package:app_flowy/core/frameless_window.dart';
  26. // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
  27. import 'package:flowy_infra/image.dart';
  28. import 'package:flowy_infra_ui/style_widget/icon_button.dart';
  29. import '../navigation.dart';
  30. import 'app/menu_app.dart';
  31. import 'app/create_button.dart';
  32. import 'menu_user.dart';
  33. class HomeMenu extends StatelessWidget {
  34. final PublishNotifier<bool> _collapsedNotifier;
  35. final UserProfilePB user;
  36. final WorkspaceSettingPB workspaceSetting;
  37. const HomeMenu({
  38. Key? key,
  39. required this.user,
  40. required this.workspaceSetting,
  41. required PublishNotifier<bool> collapsedNotifier,
  42. }) : _collapsedNotifier = collapsedNotifier,
  43. super(key: key);
  44. @override
  45. Widget build(BuildContext context) {
  46. return MultiBlocProvider(
  47. providers: [
  48. BlocProvider<MenuBloc>(
  49. create: (context) {
  50. final menuBloc = MenuBloc(
  51. user: user,
  52. workspace: workspaceSetting.workspace,
  53. );
  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.id != c.plugin.id,
  63. listener: (context, state) {
  64. getIt<HomeStackManager>().setPlugin(state.plugin);
  65. },
  66. ),
  67. BlocListener<HomeSettingBloc, HomeSettingState>(
  68. listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
  69. listener: (context, state) {
  70. _collapsedNotifier.value = state.isMenuCollapsed;
  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. return Container(
  83. decoration: BoxDecoration(
  84. color: Theme.of(context).colorScheme.surfaceVariant,
  85. border:
  86. Border(right: BorderSide(color: Theme.of(context).dividerColor)),
  87. ),
  88. child: Column(
  89. mainAxisAlignment: MainAxisAlignment.start,
  90. children: [
  91. Expanded(
  92. child: Column(
  93. mainAxisAlignment: MainAxisAlignment.start,
  94. children: [
  95. const MenuTopBar(),
  96. const VSpace(10),
  97. _renderApps(context),
  98. ],
  99. ).padding(horizontal: Insets.l),
  100. ),
  101. const VSpace(20),
  102. const MenuTrash(),
  103. const VSpace(20),
  104. _renderNewAppButton(context),
  105. ],
  106. ),
  107. );
  108. }
  109. Widget _renderApps(BuildContext context) {
  110. return ExpandableTheme(
  111. data: ExpandableThemeData(
  112. useInkWell: true, animationDuration: Durations.medium),
  113. child: Expanded(
  114. child: ScrollConfiguration(
  115. behavior: const ScrollBehavior().copyWith(scrollbars: false),
  116. child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
  117. selector: (state) => state.apps
  118. .map((app) => MenuApp(app, key: ValueKey(app.id)))
  119. .toList(),
  120. builder: (context, menuItems) {
  121. return ReorderableListView.builder(
  122. itemCount: menuItems.length,
  123. buildDefaultDragHandles: false,
  124. header: Padding(
  125. padding:
  126. EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
  127. child: MenuUser(user),
  128. ),
  129. onReorder: (oldIndex, newIndex) {
  130. // Moving item1 from index 0 to index 1
  131. // expect: oldIndex: 0, newIndex: 1
  132. // receive: oldIndex: 0, newIndex: 2
  133. // Workaround: if newIndex > oldIndex, we just minus one
  134. int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
  135. context
  136. .read<MenuBloc>()
  137. .add(MenuEvent.moveApp(oldIndex, index));
  138. },
  139. physics: StyledScrollPhysics(),
  140. itemBuilder: (BuildContext context, int index) {
  141. return ReorderableDragStartListener(
  142. key: ValueKey(menuItems[index].key),
  143. index: index,
  144. child: Padding(
  145. padding: EdgeInsets.symmetric(
  146. vertical: MenuAppSizes.appVPadding / 2),
  147. child: menuItems[index],
  148. ),
  149. );
  150. },
  151. );
  152. },
  153. ),
  154. ),
  155. ),
  156. );
  157. }
  158. Widget _renderNewAppButton(BuildContext context) {
  159. return NewAppButton(
  160. press: (appName) =>
  161. context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
  162. );
  163. }
  164. }
  165. class MenuSharedState {
  166. final ValueNotifier<ViewPB?> _latestOpenView = ValueNotifier<ViewPB?>(null);
  167. MenuSharedState({ViewPB? view}) {
  168. _latestOpenView.value = view;
  169. }
  170. ViewPB? get latestOpenView => _latestOpenView.value;
  171. ValueNotifier<ViewPB?> get notifier => _latestOpenView;
  172. set latestOpenView(ViewPB? view) {
  173. if (_latestOpenView.value != view) {
  174. _latestOpenView.value = view;
  175. }
  176. }
  177. VoidCallback addLatestViewListener(void Function(ViewPB?) callback) {
  178. listener() {
  179. callback(_latestOpenView.value);
  180. }
  181. _latestOpenView.addListener(listener);
  182. return listener;
  183. }
  184. void removeLatestViewListener(VoidCallback listener) {
  185. _latestOpenView.removeListener(listener);
  186. }
  187. }
  188. class MenuTopBar extends StatelessWidget {
  189. const MenuTopBar({Key? key}) : super(key: key);
  190. Widget renderIcon(BuildContext context) {
  191. if (Platform.isMacOS) {
  192. return Container();
  193. }
  194. return (Theme.of(context).brightness == Brightness.dark
  195. ? svgWidget("flowy_logo_dark_mode", size: const Size(92, 17))
  196. : svgWidget("flowy_logo_with_text", size: const Size(92, 17)));
  197. }
  198. @override
  199. Widget build(BuildContext context) {
  200. return BlocBuilder<MenuBloc, MenuState>(
  201. builder: (context, state) {
  202. return SizedBox(
  203. height: HomeSizes.topBarHeight,
  204. child: MoveWindowDetector(
  205. child: Row(
  206. children: [
  207. renderIcon(context),
  208. const Spacer(),
  209. Tooltip(
  210. richMessage: sidebarTooltipTextSpan(
  211. context,
  212. LocaleKeys.sideBar_closeSidebar.tr(),
  213. ),
  214. child: FlowyIconButton(
  215. width: 28,
  216. hoverColor: Colors.transparent,
  217. onPressed: () => context
  218. .read<HomeSettingBloc>()
  219. .add(const HomeSettingEvent.collapseMenu()),
  220. iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
  221. icon: svgWidget(
  222. "home/hide_menu",
  223. color: Theme.of(context).colorScheme.onSurface,
  224. ),
  225. ),
  226. )
  227. ],
  228. ),
  229. ),
  230. );
  231. },
  232. );
  233. }
  234. }