home_stack.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import 'package:appflowy/core/frameless_window.dart';
  2. import 'package:appflowy/plugins/blank/blank.dart';
  3. import 'package:appflowy/startup/plugin/plugin.dart';
  4. import 'package:appflowy/startup/startup.dart';
  5. import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
  6. import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
  7. import 'package:appflowy/workspace/presentation/home/navigation.dart';
  8. import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
  9. import 'package:appflowy/workspace/presentation/home/toast.dart';
  10. import 'package:appflowy_backend/dispatch/dispatch.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
  12. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  13. import 'package:flowy_infra_ui/style_widget/extension.dart';
  14. import 'package:flutter/material.dart';
  15. import 'package:flutter_bloc/flutter_bloc.dart';
  16. import 'package:provider/provider.dart';
  17. import 'package:time/time.dart';
  18. import 'home_layout.dart';
  19. typedef NavigationCallback = void Function(String id);
  20. abstract class HomeStackDelegate {
  21. void didDeleteStackWidget(ViewPB view, int? index);
  22. }
  23. class HomeStack extends StatelessWidget {
  24. final HomeStackDelegate delegate;
  25. final HomeLayout layout;
  26. const HomeStack({
  27. required this.delegate,
  28. required this.layout,
  29. Key? key,
  30. }) : super(key: key);
  31. @override
  32. Widget build(BuildContext context) {
  33. final pageController = PageController();
  34. return BlocProvider<TabsBloc>.value(
  35. value: getIt<TabsBloc>(),
  36. child: BlocBuilder<TabsBloc, TabsState>(
  37. builder: (context, state) {
  38. return Column(
  39. mainAxisAlignment: MainAxisAlignment.start,
  40. children: [
  41. TabsManager(pageController: pageController),
  42. state.currentPageManager.stackTopBar(layout: layout),
  43. Expanded(
  44. child: PageView(
  45. physics: const NeverScrollableScrollPhysics(),
  46. controller: pageController,
  47. children: state.pageManagers
  48. .map(
  49. (pm) => PageStack(pageManager: pm, delegate: delegate),
  50. )
  51. .toList(),
  52. ),
  53. ),
  54. ],
  55. );
  56. },
  57. ),
  58. );
  59. }
  60. }
  61. class PageStack extends StatefulWidget {
  62. const PageStack({
  63. super.key,
  64. required this.pageManager,
  65. required this.delegate,
  66. });
  67. final PageManager pageManager;
  68. final HomeStackDelegate delegate;
  69. @override
  70. State<PageStack> createState() => _PageStackState();
  71. }
  72. class _PageStackState extends State<PageStack>
  73. with AutomaticKeepAliveClientMixin {
  74. @override
  75. Widget build(BuildContext context) {
  76. super.build(context);
  77. return Container(
  78. color: Theme.of(context).colorScheme.surface,
  79. child: FocusTraversalGroup(
  80. child: widget.pageManager.stackWidget(
  81. onDeleted: (view, index) {
  82. widget.delegate.didDeleteStackWidget(view, index);
  83. },
  84. ),
  85. ),
  86. );
  87. }
  88. @override
  89. bool get wantKeepAlive => true;
  90. }
  91. class FadingIndexedStack extends StatefulWidget {
  92. final int index;
  93. final List<Widget> children;
  94. final Duration duration;
  95. const FadingIndexedStack({
  96. Key? key,
  97. required this.index,
  98. required this.children,
  99. this.duration = const Duration(
  100. milliseconds: 250,
  101. ),
  102. }) : super(key: key);
  103. @override
  104. FadingIndexedStackState createState() => FadingIndexedStackState();
  105. }
  106. class FadingIndexedStackState extends State<FadingIndexedStack> {
  107. double _targetOpacity = 1;
  108. @override
  109. void initState() {
  110. super.initState();
  111. initToastWithContext(context);
  112. }
  113. @override
  114. void didUpdateWidget(FadingIndexedStack oldWidget) {
  115. if (oldWidget.index == widget.index) return;
  116. setState(() => _targetOpacity = 0);
  117. Future.delayed(1.milliseconds, () => setState(() => _targetOpacity = 1));
  118. super.didUpdateWidget(oldWidget);
  119. }
  120. @override
  121. Widget build(BuildContext context) {
  122. return TweenAnimationBuilder<double>(
  123. duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds,
  124. tween: Tween(begin: 0, end: _targetOpacity),
  125. builder: (_, value, child) {
  126. return Opacity(opacity: value, child: child);
  127. },
  128. child: IndexedStack(index: widget.index, children: widget.children),
  129. );
  130. }
  131. }
  132. abstract mixin class NavigationItem {
  133. Widget get leftBarItem;
  134. Widget? get rightBarItem => null;
  135. Widget tabBarItem(String pluginId);
  136. NavigationCallback get action => (id) => throw UnimplementedError();
  137. }
  138. class PageNotifier extends ChangeNotifier {
  139. Plugin _plugin;
  140. Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;
  141. Widget tabBarWidget(String pluginId) =>
  142. _plugin.widgetBuilder.tabBarItem(pluginId);
  143. PageNotifier({Plugin? plugin})
  144. : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
  145. /// This is the only place where the plugin is set.
  146. /// No need compare the old plugin with the new plugin. Just set it.
  147. set plugin(Plugin newPlugin) {
  148. _plugin.dispose();
  149. /// Set the plugin view as the latest view.
  150. FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
  151. _plugin = newPlugin;
  152. notifyListeners();
  153. }
  154. Plugin get plugin => _plugin;
  155. }
  156. // PageManager manages the view for one Tab
  157. class PageManager {
  158. final PageNotifier _notifier = PageNotifier();
  159. PageNotifier get notifier => _notifier;
  160. PageManager();
  161. Widget title() {
  162. return _notifier.plugin.widgetBuilder.leftBarItem;
  163. }
  164. Plugin get plugin => _notifier.plugin;
  165. void setPlugin(Plugin newPlugin) {
  166. _notifier.plugin = newPlugin;
  167. }
  168. void setStackWithId(String id) {
  169. // Navigate to the page with id
  170. }
  171. Widget stackTopBar({required HomeLayout layout}) {
  172. return MultiProvider(
  173. providers: [
  174. ChangeNotifierProvider.value(value: _notifier),
  175. ],
  176. child: Selector<PageNotifier, Widget>(
  177. selector: (context, notifier) => notifier.titleWidget,
  178. builder: (context, widget, child) {
  179. return MoveWindowDetector(child: HomeTopBar(layout: layout));
  180. },
  181. ),
  182. );
  183. }
  184. Widget stackWidget({required Function(ViewPB, int?) onDeleted}) {
  185. return MultiProvider(
  186. providers: [ChangeNotifierProvider.value(value: _notifier)],
  187. child: Consumer(
  188. builder: (_, PageNotifier notifier, __) {
  189. return FadingIndexedStack(
  190. index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
  191. children: getIt<PluginSandbox>().supportPluginTypes.map(
  192. (pluginType) {
  193. if (pluginType == notifier.plugin.pluginType) {
  194. final builder = notifier.plugin.widgetBuilder;
  195. final pluginWidget = builder.buildWidget(
  196. context: PluginContext(onDeleted: onDeleted),
  197. );
  198. return Padding(
  199. padding: builder.contentPadding,
  200. child: pluginWidget,
  201. );
  202. }
  203. return const BlankPage();
  204. },
  205. ).toList(),
  206. );
  207. },
  208. ),
  209. );
  210. }
  211. }
  212. class HomeTopBar extends StatelessWidget {
  213. const HomeTopBar({Key? key, required this.layout}) : super(key: key);
  214. final HomeLayout layout;
  215. @override
  216. Widget build(BuildContext context) {
  217. return Container(
  218. color: Theme.of(context).colorScheme.onSecondaryContainer,
  219. height: HomeSizes.topBarHeight,
  220. child: Padding(
  221. padding: const EdgeInsets.symmetric(
  222. horizontal: HomeInsets.topBarTitlePadding,
  223. ),
  224. child: Row(
  225. crossAxisAlignment: CrossAxisAlignment.center,
  226. children: [
  227. HSpace(layout.menuSpacing),
  228. const FlowyNavigation(),
  229. const HSpace(16),
  230. ChangeNotifierProvider.value(
  231. value: Provider.of<PageNotifier>(context, listen: false),
  232. child: Consumer(
  233. builder: (_, PageNotifier notifier, __) =>
  234. notifier.plugin.widgetBuilder.rightBarItem ??
  235. const SizedBox.shrink(),
  236. ),
  237. ),
  238. ],
  239. ),
  240. ).bottomBorder(color: Theme.of(context).dividerColor),
  241. );
  242. }
  243. }