home_stack.dart 8.2 KB

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