import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/navigation.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; import 'home_layout.dart'; typedef NavigationCallback = void Function(String id); abstract class HomeStackDelegate { void didDeleteStackWidget(ViewPB view, int? index); } class HomeStack extends StatelessWidget { final HomeStackDelegate delegate; final HomeLayout layout; const HomeStack({ required this.delegate, required this.layout, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ getIt().stackTopBar(layout: layout), Expanded( child: Container( color: Theme.of(context).colorScheme.surface, child: FocusTraversalGroup( child: getIt().stackWidget( onDeleted: (view, index) { delegate.didDeleteStackWidget(view, index); }, ), ), ), ), ], ); } } class FadingIndexedStack extends StatefulWidget { final int index; final List children; final Duration duration; const FadingIndexedStack({ Key? key, required this.index, required this.children, this.duration = const Duration( milliseconds: 250, ), }) : super(key: key); @override FadingIndexedStackState createState() => FadingIndexedStackState(); } class FadingIndexedStackState extends State { double _targetOpacity = 1; @override void initState() { super.initState(); initToastWithContext(context); } @override void didUpdateWidget(FadingIndexedStack oldWidget) { if (oldWidget.index == widget.index) return; setState(() => _targetOpacity = 0); Future.delayed(1.milliseconds, () => setState(() => _targetOpacity = 1)); super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return TweenAnimationBuilder( duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds, tween: Tween(begin: 0, end: _targetOpacity), builder: (_, value, child) { return Opacity(opacity: value, child: child); }, child: IndexedStack(index: widget.index, children: widget.children), ); } } abstract mixin class NavigationItem { Widget get leftBarItem; Widget? get rightBarItem => null; NavigationCallback get action => (id) { getIt().setStackWithId(id); }; } class HomeStackNotifier extends ChangeNotifier { Plugin _plugin; Widget get titleWidget => _plugin.widgetBuilder.leftBarItem; HomeStackNotifier({Plugin? plugin}) : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank); /// This is the only place where the plugin is set. /// No need compare the old plugin with the new plugin. Just set it. set plugin(Plugin newPlugin) { _plugin.dispose(); /// Set the plugin view as the latest view. FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); _plugin = newPlugin; notifyListeners(); } Plugin get plugin => _plugin; } // HomeStack is initialized as singleton to control the page stack. class HomeStackManager { final HomeStackNotifier _notifier = HomeStackNotifier(); HomeStackManager(); Widget title() { return _notifier.plugin.widgetBuilder.leftBarItem; } Plugin get plugin => _notifier.plugin; void setPlugin(Plugin newPlugin) { _notifier.plugin = newPlugin; } void setStackWithId(String id) { // Navigate to the page with id } Widget stackTopBar({required HomeLayout layout}) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: _notifier), ], child: Selector( selector: (context, notifier) => notifier.titleWidget, builder: (context, widget, child) { return MoveWindowDetector(child: HomeTopBar(layout: layout)); }, ), ); } Widget stackWidget({required Function(ViewPB, int?) onDeleted}) { return MultiProvider( providers: [ChangeNotifierProvider.value(value: _notifier)], child: Consumer( builder: (_, HomeStackNotifier notifier, __) { return FadingIndexedStack( index: getIt().indexOf(notifier.plugin.pluginType), children: getIt().supportPluginTypes.map( (pluginType) { if (pluginType == notifier.plugin.pluginType) { final builder = notifier.plugin.widgetBuilder; final pluginWidget = builder.buildWidget( context: PluginContext(onDeleted: onDeleted), ); return Padding( padding: builder.contentPadding, child: pluginWidget, ); } else { return const BlankPage(); } }, ).toList(), ); }, ), ); } } class HomeTopBar extends StatelessWidget { const HomeTopBar({Key? key, required this.layout}) : super(key: key); final HomeLayout layout; @override Widget build(BuildContext context) { return Container( color: Theme.of(context).colorScheme.onSecondaryContainer, height: HomeSizes.topBarHeight, child: Padding( padding: const EdgeInsets.symmetric( horizontal: HomeInsets.topBarTitlePadding, ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ HSpace(layout.menuSpacing), const FlowyNavigation(), const HSpace(16), ChangeNotifierProvider.value( value: Provider.of(context, listen: false), child: Consumer( builder: (_, HomeStackNotifier notifier, __) => notifier.plugin.widgetBuilder.rightBarItem ?? const SizedBox.shrink(), ), ), ], ), ).bottomBorder(color: Theme.of(context).dividerColor), ); } }