menu.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import 'dart:io' show Platform;
  2. import 'package:appflowy/core/frameless_window.dart';
  3. import 'package:appflowy/generated/locale_keys.g.dart';
  4. import 'package:appflowy/plugins/trash/menu.dart';
  5. import 'package:appflowy/startup/startup.dart';
  6. import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
  7. import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
  8. import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
  9. import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
  12. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
  13. show UserProfilePB;
  14. import 'package:easy_localization/easy_localization.dart';
  15. import 'package:expandable/expandable.dart';
  16. import 'package:flowy_infra/image.dart';
  17. import 'package:flowy_infra/size.dart';
  18. import 'package:flowy_infra/time/duration.dart';
  19. import 'package:flowy_infra_ui/style_widget/icon_button.dart';
  20. import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
  21. import 'package:flowy_infra_ui/widget/spacing.dart';
  22. import 'package:flutter/material.dart';
  23. import 'package:flutter_bloc/flutter_bloc.dart';
  24. import 'package:styled_widget/styled_widget.dart';
  25. import '../navigation.dart';
  26. import 'app/create_button.dart';
  27. import 'app/menu_app.dart';
  28. import 'menu_user.dart';
  29. export './app/header/header.dart';
  30. export './app/menu_app.dart';
  31. class HomeMenu extends StatelessWidget {
  32. final UserProfilePB user;
  33. final WorkspaceSettingPB workspaceSetting;
  34. const HomeMenu({
  35. Key? key,
  36. required this.user,
  37. required this.workspaceSetting,
  38. }) : super(key: key);
  39. @override
  40. Widget build(BuildContext context) {
  41. return MultiBlocProvider(
  42. providers: [
  43. BlocProvider<MenuBloc>(
  44. create: (context) {
  45. final menuBloc = MenuBloc(
  46. user: user,
  47. workspace: workspaceSetting.workspace,
  48. );
  49. menuBloc.add(const MenuEvent.initial());
  50. return menuBloc;
  51. },
  52. ),
  53. ],
  54. child: MultiBlocListener(
  55. listeners: [
  56. BlocListener<MenuBloc, MenuState>(
  57. listenWhen: (p, c) => p.plugin.id != c.plugin.id,
  58. listener: (context, state) {
  59. getIt<TabsBloc>().add(
  60. TabsEvent.openPlugin(plugin: state.plugin),
  61. );
  62. },
  63. ),
  64. ],
  65. child: BlocBuilder<MenuBloc, MenuState>(
  66. builder: (context, state) => _renderBody(context),
  67. ),
  68. ),
  69. );
  70. }
  71. Widget _renderBody(BuildContext context) {
  72. // nested column: https://siddharthmolleti.com/flutter-box-constraints-nested-column-s-row-s-3dfacada7361
  73. return Container(
  74. decoration: BoxDecoration(
  75. color: Theme.of(context).colorScheme.surfaceVariant,
  76. border:
  77. Border(right: BorderSide(color: Theme.of(context).dividerColor)),
  78. ),
  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(),
  94. const VSpace(20),
  95. _renderNewAppButton(context),
  96. ],
  97. ),
  98. );
  99. }
  100. Widget _renderApps(BuildContext context) {
  101. return ExpandableTheme(
  102. data: ExpandableThemeData(
  103. useInkWell: true,
  104. animationDuration: Durations.medium,
  105. ),
  106. child: Expanded(
  107. child: ScrollConfiguration(
  108. behavior: const ScrollBehavior().copyWith(scrollbars: false),
  109. child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
  110. selector: (state) => state.views
  111. .map((app) => MenuApp(app, key: ValueKey(app.id)))
  112. .toList(),
  113. builder: (context, menuItems) {
  114. return ReorderableListView.builder(
  115. itemCount: menuItems.length,
  116. buildDefaultDragHandles: false,
  117. header: Padding(
  118. padding:
  119. EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
  120. child: MenuUser(user),
  121. ),
  122. onReorder: (oldIndex, newIndex) {
  123. // Moving item1 from index 0 to index 1
  124. // expect: oldIndex: 0, newIndex: 1
  125. // receive: oldIndex: 0, newIndex: 2
  126. // Workaround: if newIndex > oldIndex, we just minus one
  127. final int index =
  128. 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. ),
  142. child: menuItems[index],
  143. ),
  144. );
  145. },
  146. proxyDecorator: (child, index, animation) =>
  147. Material(color: Colors.transparent, child: child),
  148. );
  149. },
  150. ),
  151. ),
  152. ),
  153. );
  154. }
  155. Widget _renderNewAppButton(BuildContext context) {
  156. return NewAppButton(
  157. press: (appName) =>
  158. context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
  159. );
  160. }
  161. }
  162. class MenuSharedState {
  163. final ValueNotifier<ViewPB?> _latestOpenView = ValueNotifier<ViewPB?>(null);
  164. MenuSharedState({ViewPB? view}) {
  165. _latestOpenView.value = view;
  166. }
  167. ViewPB? get latestOpenView => _latestOpenView.value;
  168. ValueNotifier<ViewPB?> get notifier => _latestOpenView;
  169. set latestOpenView(ViewPB? view) {
  170. if (_latestOpenView.value != view) {
  171. _latestOpenView.value = view;
  172. }
  173. }
  174. VoidCallback addLatestViewListener(void Function(ViewPB?) callback) {
  175. listener() {
  176. callback(_latestOpenView.value);
  177. }
  178. _latestOpenView.addListener(listener);
  179. return listener;
  180. }
  181. void removeLatestViewListener(VoidCallback listener) {
  182. _latestOpenView.removeListener(listener);
  183. }
  184. }
  185. class MenuTopBar extends StatelessWidget {
  186. const MenuTopBar({Key? key}) : super(key: key);
  187. Widget renderIcon(BuildContext context) {
  188. if (Platform.isMacOS) {
  189. return Container();
  190. }
  191. return (Theme.of(context).brightness == Brightness.dark
  192. ? svgWidget("flowy_logo_dark_mode", size: const Size(92, 17))
  193. : svgWidget("flowy_logo_with_text", size: const Size(92, 17)));
  194. }
  195. @override
  196. Widget build(BuildContext context) {
  197. return BlocBuilder<MenuBloc, MenuState>(
  198. builder: (context, state) {
  199. return SizedBox(
  200. height: HomeSizes.topBarHeight,
  201. child: MoveWindowDetector(
  202. child: Row(
  203. children: [
  204. renderIcon(context),
  205. const Spacer(),
  206. Tooltip(
  207. richMessage: sidebarTooltipTextSpan(
  208. context,
  209. LocaleKeys.sideBar_closeSidebar.tr(),
  210. ),
  211. child: FlowyIconButton(
  212. width: 28,
  213. hoverColor: Colors.transparent,
  214. onPressed: () => context
  215. .read<HomeSettingBloc>()
  216. .add(const HomeSettingEvent.collapseMenu()),
  217. iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
  218. icon: svgWidget(
  219. "home/hide_menu",
  220. color: Theme.of(context).iconTheme.color,
  221. ),
  222. ),
  223. )
  224. ],
  225. ),
  226. ),
  227. );
  228. },
  229. );
  230. }
  231. }