menu.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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/favorite/favorite_bloc.dart';
  7. import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
  8. import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
  9. import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
  10. import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  12. import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
  13. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
  14. show UserProfilePB;
  15. import 'package:easy_localization/easy_localization.dart';
  16. import 'package:expandable/expandable.dart';
  17. import 'package:flowy_infra/image.dart';
  18. import 'package:flowy_infra/size.dart';
  19. import 'package:flowy_infra/time/duration.dart';
  20. import 'package:flowy_infra_ui/style_widget/icon_button.dart';
  21. import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
  22. import 'package:flowy_infra_ui/widget/spacing.dart';
  23. import 'package:flutter/material.dart';
  24. import 'package:flutter_bloc/flutter_bloc.dart';
  25. import 'package:styled_widget/styled_widget.dart';
  26. import '../navigation.dart';
  27. import 'app/create_button.dart';
  28. import 'app/menu_app.dart';
  29. import 'app/section/item.dart';
  30. import 'menu_user.dart';
  31. export './app/header/header.dart';
  32. export './app/menu_app.dart';
  33. class HomeMenu extends StatelessWidget {
  34. final UserProfilePB user;
  35. final WorkspaceSettingPB workspaceSetting;
  36. const HomeMenu({
  37. Key? key,
  38. required this.user,
  39. required this.workspaceSetting,
  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 = MenuBloc(
  48. user: user,
  49. workspace: workspaceSetting.workspace,
  50. );
  51. menuBloc.add(const MenuEvent.initial());
  52. return menuBloc;
  53. },
  54. ),
  55. BlocProvider(
  56. create: (ctx) =>
  57. getIt<FavoriteBloc>()..add(const FavoriteEvent.initial()),
  58. )
  59. ],
  60. child: MultiBlocListener(
  61. listeners: [
  62. BlocListener<MenuBloc, MenuState>(
  63. listenWhen: (p, c) => p.plugin.id != c.plugin.id,
  64. listener: (context, state) {
  65. getIt<TabsBloc>().add(
  66. TabsEvent.openPlugin(plugin: state.plugin),
  67. );
  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. return Container(
  80. decoration: BoxDecoration(
  81. color: Theme.of(context).colorScheme.surfaceVariant,
  82. border:
  83. Border(right: BorderSide(color: Theme.of(context).dividerColor)),
  84. ),
  85. child: Column(
  86. mainAxisAlignment: MainAxisAlignment.start,
  87. children: [
  88. Expanded(
  89. child: Column(
  90. mainAxisAlignment: MainAxisAlignment.start,
  91. children: [
  92. const MenuTopBar(),
  93. const VSpace(10),
  94. _renderApps(context),
  95. ],
  96. ).padding(horizontal: Insets.l),
  97. ),
  98. const VSpace(20),
  99. const MenuTrash(),
  100. const VSpace(20),
  101. _renderNewAppButton(context),
  102. ],
  103. ),
  104. );
  105. }
  106. Widget _renderFavorites(BuildContext context) {
  107. return BlocBuilder<FavoriteBloc, FavoriteState>(
  108. builder: (context, state) {
  109. return state.views.isNotEmpty
  110. ? ExpandableTheme(
  111. data: ExpandableThemeData(
  112. useInkWell: true,
  113. animationDuration: Durations.medium,
  114. ),
  115. child: ExpandablePanel(
  116. theme: const ExpandableThemeData(
  117. headerAlignment: ExpandablePanelHeaderAlignment.center,
  118. tapBodyToExpand: false,
  119. tapBodyToCollapse: false,
  120. tapHeaderToExpand: false,
  121. iconPadding: EdgeInsets.zero,
  122. hasIcon: false,
  123. ),
  124. // header: const FavoriteHeader(),
  125. expanded: ScrollConfiguration(
  126. behavior:
  127. const ScrollBehavior().copyWith(scrollbars: false),
  128. child: Column(
  129. children: state.views
  130. .map(
  131. (e) => ViewSectionItem(
  132. key: ValueKey(e.id),
  133. isSelected: false,
  134. onSelected: (view) => getIt<MenuSharedState>()
  135. .latestOpenView = view,
  136. view: e,
  137. ),
  138. )
  139. .toList(),
  140. ),
  141. ),
  142. collapsed: const SizedBox.shrink(),
  143. ),
  144. )
  145. : const SizedBox.shrink();
  146. },
  147. );
  148. }
  149. Widget _renderApps(BuildContext context) {
  150. return ExpandableTheme(
  151. data: ExpandableThemeData(
  152. useInkWell: true,
  153. animationDuration: Durations.medium,
  154. ),
  155. child: Expanded(
  156. child: ScrollConfiguration(
  157. behavior: const ScrollBehavior().copyWith(scrollbars: false),
  158. child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
  159. selector: (state) => state.views
  160. .map((app) => MenuApp(app, key: ValueKey(app.id)))
  161. .toList(),
  162. builder: (context, menuItems) {
  163. return ReorderableListView.builder(
  164. itemCount: menuItems.length,
  165. buildDefaultDragHandles: false,
  166. header: Column(
  167. children: [
  168. Padding(
  169. padding: EdgeInsets.only(
  170. bottom: MenuAppSizes.appVPadding,
  171. ),
  172. child: MenuUser(user),
  173. ),
  174. _renderFavorites(context),
  175. ],
  176. ),
  177. onReorder: (oldIndex, newIndex) {
  178. // Moving item1 from index 0 to index 1
  179. // expect: oldIndex: 0, newIndex: 1
  180. // receive: oldIndex: 0, newIndex: 2
  181. // Workaround: if newIndex > oldIndex, we just minus one
  182. final int index =
  183. newIndex > oldIndex ? newIndex - 1 : newIndex;
  184. context
  185. .read<MenuBloc>()
  186. .add(MenuEvent.moveApp(oldIndex, index));
  187. },
  188. physics: StyledScrollPhysics(),
  189. itemBuilder: (BuildContext context, int index) {
  190. return ReorderableDragStartListener(
  191. key: ValueKey(menuItems[index].key),
  192. index: index,
  193. child: Padding(
  194. padding: EdgeInsets.symmetric(
  195. vertical: MenuAppSizes.appVPadding / 2,
  196. ),
  197. child: menuItems[index],
  198. ),
  199. );
  200. },
  201. proxyDecorator: (child, index, animation) =>
  202. Material(color: Colors.transparent, child: child),
  203. );
  204. },
  205. ),
  206. ),
  207. ),
  208. );
  209. }
  210. Widget _renderNewAppButton(BuildContext context) {
  211. return NewAppButton(
  212. press: (appName) =>
  213. context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
  214. );
  215. }
  216. }
  217. class MenuSharedState {
  218. final ValueNotifier<ViewPB?> _latestOpenView = ValueNotifier<ViewPB?>(null);
  219. MenuSharedState({ViewPB? view}) {
  220. _latestOpenView.value = view;
  221. }
  222. ViewPB? get latestOpenView => _latestOpenView.value;
  223. ValueNotifier<ViewPB?> get notifier => _latestOpenView;
  224. set latestOpenView(ViewPB? view) {
  225. if (_latestOpenView.value?.id != view?.id) {
  226. _latestOpenView.value = view;
  227. }
  228. }
  229. VoidCallback addLatestViewListener(void Function(ViewPB?) callback) {
  230. listener() {
  231. callback(_latestOpenView.value);
  232. }
  233. _latestOpenView.addListener(listener);
  234. return listener;
  235. }
  236. void removeLatestViewListener(VoidCallback listener) {
  237. _latestOpenView.removeListener(listener);
  238. }
  239. }
  240. class MenuTopBar extends StatelessWidget {
  241. const MenuTopBar({Key? key}) : super(key: key);
  242. Widget renderIcon(BuildContext context) {
  243. if (Platform.isMacOS) {
  244. return Container();
  245. }
  246. return (Theme.of(context).brightness == Brightness.dark
  247. ? svgWidget("flowy_logo_dark_mode", size: const Size(92, 17))
  248. : svgWidget("flowy_logo_with_text", size: const Size(92, 17)));
  249. }
  250. @override
  251. Widget build(BuildContext context) {
  252. return BlocBuilder<MenuBloc, MenuState>(
  253. builder: (context, state) {
  254. return SizedBox(
  255. height: HomeSizes.topBarHeight,
  256. child: MoveWindowDetector(
  257. child: Row(
  258. children: [
  259. renderIcon(context),
  260. const Spacer(),
  261. Tooltip(
  262. richMessage: sidebarTooltipTextSpan(
  263. context,
  264. LocaleKeys.sideBar_closeSidebar.tr(),
  265. ),
  266. child: FlowyIconButton(
  267. width: 28,
  268. hoverColor: Colors.transparent,
  269. onPressed: () => context
  270. .read<HomeSettingBloc>()
  271. .add(const HomeSettingEvent.collapseMenu()),
  272. iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
  273. icon: svgWidget(
  274. "home/hide_menu",
  275. color: Theme.of(context).iconTheme.color,
  276. ),
  277. ),
  278. )
  279. ],
  280. ),
  281. ),
  282. );
  283. },
  284. );
  285. }
  286. }