浏览代码

[flutter]: right click action for menu's app

appflowy 3 年之前
父节点
当前提交
dd9456e7ff

+ 19 - 0
app_flowy/lib/startup/tasks/application_task.dart

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:window_size/window_size.dart';
 import 'package:app_flowy/startup/launcher.dart';
+import 'package:bloc/bloc.dart';
+import 'package:flowy_log/flowy_log.dart';
 
 class AppWidgetTask extends LaunchTask {
   @override
@@ -14,6 +16,7 @@ class AppWidgetTask extends LaunchTask {
   Future<void> initialize(LaunchContext context) {
     final widget = context.getIt<EntryPoint>().create();
     final app = ApplicationWidget(child: widget);
+    Bloc.observer = ApplicationBlocObserver();
     runApp(app);
 
     return Future(() => {});
@@ -53,3 +56,19 @@ class AppGlobals {
   static GlobalKey<NavigatorState> rootNavKey = GlobalKey();
   static NavigatorState get nav => rootNavKey.currentState!;
 }
+
+class ApplicationBlocObserver extends BlocObserver {
+  @override
+  // ignore: unnecessary_overrides
+  void onTransition(Bloc bloc, Transition transition) {
+    // Log.debug("[current]: ${transition.currentState} \n\n[next]: ${transition.nextState}");
+    Log.debug("${transition.nextState}");
+    super.onTransition(bloc, transition);
+  }
+
+  @override
+  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
+    Log.debug(error);
+    super.onError(bloc, error, stackTrace);
+  }
+}

+ 0 - 19
app_flowy/lib/startup/tasks/sdk_task.dart

@@ -1,11 +1,9 @@
 import 'dart:io';
 import 'package:app_flowy/startup/launcher.dart';
 import 'package:app_flowy/startup/startup.dart';
-import 'package:bloc/bloc.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:flowy_sdk/flowy_sdk.dart';
 import 'package:flutter/material.dart';
-import 'package:flowy_log/flowy_log.dart';
 
 class RustSDKInitTask extends LaunchTask {
   @override
@@ -15,8 +13,6 @@ class RustSDKInitTask extends LaunchTask {
   Future<void> initialize(LaunchContext context) async {
     WidgetsFlutterBinding.ensureInitialized();
 
-    Bloc.observer = ApplicationBlocObserver();
-
     Directory directory = await getApplicationDocumentsDirectory();
     final documentPath = directory.path;
 
@@ -35,18 +31,3 @@ class RustSDKInitTask extends LaunchTask {
     });
   }
 }
-
-class ApplicationBlocObserver extends BlocObserver {
-  @override
-  // ignore: unnecessary_overrides
-  void onTransition(Bloc bloc, Transition transition) {
-    // Log.debug("[current]: ${transition.currentState} \n[next]: ${transition.nextState}");
-    super.onTransition(bloc, transition);
-  }
-
-  @override
-  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
-    Log.debug(error);
-    super.onError(bloc, error, stackTrace);
-  }
-}

+ 27 - 0
app_flowy/lib/workspace/domain/edit_action/app_edit.dart

@@ -0,0 +1,27 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+enum AppDisclosureAction {
+  rename,
+  delete,
+}
+
+extension AppDisclosureExtension on AppDisclosureAction {
+  String get name {
+    switch (this) {
+      case AppDisclosureAction.rename:
+        return 'rename';
+      case AppDisclosureAction.delete:
+        return 'delete';
+    }
+  }
+
+  Widget get icon {
+    switch (this) {
+      case AppDisclosureAction.rename:
+        return svg('editor/edit');
+      case AppDisclosureAction.delete:
+        return svg('editor/delete');
+    }
+  }
+}

+ 8 - 8
app_flowy/lib/workspace/domain/view_edit.dart → app_flowy/lib/workspace/domain/edit_action/view_edit.dart

@@ -1,31 +1,31 @@
 import 'package:flowy_infra/image.dart';
 import 'package:flutter/material.dart';
 
-enum ViewAction {
+enum ViewDisclosureAction {
   rename,
   delete,
   duplicate,
 }
 
-extension ViewActionExtension on ViewAction {
+extension ViewDisclosureExtension on ViewDisclosureAction {
   String get name {
     switch (this) {
-      case ViewAction.rename:
+      case ViewDisclosureAction.rename:
         return 'rename';
-      case ViewAction.delete:
+      case ViewDisclosureAction.delete:
         return 'delete';
-      case ViewAction.duplicate:
+      case ViewDisclosureAction.duplicate:
         return 'duplicate';
     }
   }
 
   Widget get icon {
     switch (this) {
-      case ViewAction.rename:
+      case ViewDisclosureAction.rename:
         return svg('editor/edit');
-      case ViewAction.delete:
+      case ViewDisclosureAction.delete:
         return svg('editor/delete');
-      case ViewAction.duplicate:
+      case ViewDisclosureAction.duplicate:
         return svg('editor/copy');
     }
   }

+ 2 - 3
app_flowy/lib/workspace/presentation/widgets/menu/menu.dart

@@ -128,8 +128,7 @@ class HomeMenu extends StatelessWidget {
     List<MenuItem> items = [];
     items.add(MenuUser(user));
 
-    List<MenuItem> appWidgets =
-        apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(MenuAppContext(app))).toList());
+    List<MenuItem> appWidgets = apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList());
 
     items.addAll(appWidgets);
     return items;
@@ -166,7 +165,7 @@ class MenuList extends StatelessWidget {
               if (index == 0) {
                 return const VSpace(29);
               } else {
-                return const VSpace(24);
+                return VSpace(MenuAppSizes.appVPadding);
               }
             },
             physics: StyledScrollPhysics(),

+ 0 - 62
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header.dart → app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/add_button.dart

@@ -1,76 +1,14 @@
-import 'package:expandable/expandable.dart';
-import 'package:flowy_infra/flowy_icon_data_icons.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 
-import 'package:app_flowy/workspace/application/app/app_bloc.dart';
-
-import 'menu_app.dart';
-
-class MenuAppHeader extends StatelessWidget {
-  final App app;
-  const MenuAppHeader(
-    this.app, {
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return SizedBox(
-      height: 20,
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.start,
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          InkWell(
-            onTap: () {
-              ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
-            },
-            child: ExpandableIcon(
-              theme: ExpandableThemeData(
-                expandIcon: FlowyIconData.drop_down_show,
-                collapseIcon: FlowyIconData.drop_down_hide,
-                iconColor: theme.shader1,
-                iconSize: MenuAppSizes.expandedIconSize,
-                iconPadding: EdgeInsets.zero,
-                hasIcon: false,
-              ),
-            ),
-          ),
-          HSpace(MenuAppSizes.expandedIconPadding),
-          Expanded(
-              child: GestureDetector(
-            behavior: HitTestBehavior.opaque,
-            onTapDown: (_) {
-              ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
-            },
-            child: FlowyText.medium(
-              app.name,
-              fontSize: 12,
-            ),
-          )),
-          AddButton(
-            onSelected: (viewType) {
-              context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
-            },
-          ).padding(right: MenuAppSizes.expandedIconPadding),
-        ],
-      ),
-    );
-  }
-}
-
 class AddButton extends StatelessWidget {
   final Function(ViewType) onSelected;
   const AddButton({

+ 93 - 0
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart

@@ -0,0 +1,93 @@
+import 'package:expandable/expandable.dart';
+import 'package:flowy_infra/flowy_icon_data_icons.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+import '../menu_app.dart';
+import 'add_button.dart';
+import 'right_click_action.dart';
+
+class MenuAppHeader extends StatelessWidget {
+  final App app;
+  const MenuAppHeader(
+    this.app, {
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return SizedBox(
+      height: MenuAppSizes.headerHeight,
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          _renderExpandedIcon(context, theme),
+          HSpace(MenuAppSizes.iconPadding),
+          _renderTitle(context),
+          _renderAddButton(context),
+        ],
+      ),
+    );
+  }
+
+  Widget _renderExpandedIcon(BuildContext context, AppTheme theme) {
+    return SizedBox(
+      width: MenuAppSizes.headerHeight,
+      height: MenuAppSizes.headerHeight,
+      child: InkWell(
+        onTap: () {
+          ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
+        },
+        child: ExpandableIcon(
+          theme: ExpandableThemeData(
+            expandIcon: FlowyIconData.drop_down_show,
+            collapseIcon: FlowyIconData.drop_down_hide,
+            iconColor: theme.shader1,
+            iconSize: MenuAppSizes.iconSize,
+            iconPadding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
+            hasIcon: false,
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _renderTitle(BuildContext context) {
+    return Expanded(
+      child: GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTap: () {
+          // Open the document
+          ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
+        },
+        onSecondaryTap: () {
+          AppDisclosureActions(onSelected: (action) {
+            print(action);
+          }).show(context, context, anchorDirection: AnchorDirection.bottomWithCenterAligned);
+        },
+        child: FlowyText.medium(
+          app.name,
+          fontSize: 12,
+        ),
+      ),
+    );
+  }
+
+  Widget _renderAddButton(BuildContext context) {
+    return AddButton(
+      onSelected: (viewType) {
+        context.read<AppBloc>().add(AppEvent.createView("New view", "", viewType));
+      },
+    ).padding(right: MenuAppSizes.headerPadding);
+  }
+}

+ 50 - 0
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/right_click_action.dart

@@ -0,0 +1,50 @@
+import 'package:app_flowy/workspace/domain/edit_action/app_edit.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:dartz/dartz.dart' as dartz;
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class AppDisclosureActions with ActionList<AppDisclosureActionWrapper> implements FlowyOverlayDelegate {
+  final Function(dartz.Option<AppDisclosureAction>) onSelected;
+  final _items = AppDisclosureAction.values.map((action) => AppDisclosureActionWrapper(action)).toList();
+
+  AppDisclosureActions({
+    required this.onSelected,
+  });
+
+  @override
+  String get identifier => "ViewDisclosureActions";
+
+  @override
+  List<AppDisclosureActionWrapper> get items => _items;
+
+  @override
+  double get maxWidth => 162;
+
+  @override
+  void Function(dartz.Option<AppDisclosureActionWrapper> p1) get selectCallback => (result) {
+        result.fold(
+          () => onSelected(dartz.none()),
+          (wrapper) => onSelected(dartz.some(wrapper.inner)),
+        );
+      };
+
+  @override
+  FlowyOverlayDelegate? get delegate => this;
+
+  @override
+  void didRemove() {
+    onSelected(dartz.none());
+  }
+}
+
+class AppDisclosureActionWrapper extends ActionItemData {
+  final AppDisclosureAction inner;
+
+  AppDisclosureActionWrapper(this.inner);
+  @override
+  Widget get icon => inner.icon;
+
+  @override
+  String get name => inner.name;
+}

+ 22 - 55
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header.dart';
+import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header/header.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
@@ -10,36 +10,12 @@ import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
 import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'section/section.dart';
-// [[diagram: MenuApp]]
-//                     ┌────────┐
-//               ┌────▶│AppBloc │────────────────┐
-//               │     └────────┘                │
-//               │                               │
-//               │ 1.1 fetch views               │
-//               │ 1.2 update the MenuAppContext │
-//               │ with the views                │
-//               │                               ▼      3.render sections
-// ┌────────┐    │                       ┌──────────────┐     ┌──────────────┐
-// │MenuApp │────┤                       │MenuAppContext│─┬──▶│ ViewSection  │────────────────┐
-// └────────┘    │                       └──────────────┘ │   └──────────────┘                │
-//               │                               ▲        │                                   │
-//               │                               │        │                                   │
-//               │                               │   hold │                                   │
-//               │                               │        │                     bind          ▼
-//               │                               │        │  ┌─────────────────┐   ┌────────────────────┐
-//               │    ┌──────────────┐           │        └─▶│ViewListNotifier │──▶│ViewSectionNotifier │
-//               └───▶│AppListenBloc │───────────┘           └─────────────────┘   └────────────────────┘
-//                    └──────────────┘
-//                                                                    4.notifier binding. So The ViewSection
-//                 2.1 listen on the app                              will be re rebuild if the the number of
-//                 2.2 notify if the number of the app's view         the views in MenuAppContext was changed.
-//                 was changed
-//                 2.3 update MenuAppContext with the new
-//                 views
 
 class MenuApp extends MenuItem {
-  final MenuAppContext appCtx;
-  MenuApp(this.appCtx, {Key? key}) : super(key: appCtx.valueKey());
+  final App app;
+  final notifier = AppDataNotifier();
+
+  MenuApp(this.app, {Key? key}) : super(key: ValueKey("${app.id}${app.version}"));
 
   @override
   Widget build(BuildContext context) {
@@ -47,7 +23,7 @@ class MenuApp extends MenuItem {
       providers: [
         BlocProvider<AppBloc>(
           create: (context) {
-            final appBloc = getIt<AppBloc>(param1: appCtx.app.id);
+            final appBloc = getIt<AppBloc>(param1: app.id);
             appBloc.add(const AppEvent.initial());
             return appBloc;
           },
@@ -55,13 +31,12 @@ class MenuApp extends MenuItem {
       ],
       child: BlocListener<AppBloc, AppState>(
         listenWhen: (p, c) => p.selectedView != c.selectedView,
-        listener: (context, state) => appCtx.viewList.selectView = state.selectedView,
+        listener: (context, state) => notifier.selectView = state.selectedView,
         child: BlocBuilder<AppBloc, AppState>(
           buildWhen: (p, c) => p.views != c.views,
           builder: (context, state) {
-            appCtx.viewList.views = state.views;
-            final child = _renderViewSection(appCtx.viewList);
-            return expandableWrapper(context, child);
+            notifier.views = state.views;
+            return expandableWrapper(context, _renderViewSection(notifier));
           },
         ),
       ),
@@ -71,7 +46,7 @@ class MenuApp extends MenuItem {
   ExpandableNotifier expandableWrapper(BuildContext context, Widget child) {
     return ExpandableNotifier(
       child: ScrollOnExpand(
-        scrollOnExpand: true,
+        scrollOnExpand: false,
         scrollOnCollapse: false,
         child: Column(
           children: <Widget>[
@@ -84,7 +59,7 @@ class MenuApp extends MenuItem {
                 iconPadding: EdgeInsets.zero,
                 hasIcon: false,
               ),
-              header: MenuAppHeader(appCtx.app),
+              header: MenuAppHeader(app),
               expanded: child,
               collapsed: const SizedBox(),
             ),
@@ -94,12 +69,10 @@ class MenuApp extends MenuItem {
     );
   }
 
-  Widget _renderViewSection(ViewListNotifier viewListNotifier) {
+  Widget _renderViewSection(AppDataNotifier viewListNotifier) {
     return MultiProvider(
-      providers: [
-        ChangeNotifierProvider.value(value: viewListNotifier),
-      ],
-      child: Consumer(builder: (context, ViewListNotifier notifier, child) {
+      providers: [ChangeNotifierProvider.value(value: viewListNotifier)],
+      child: Consumer(builder: (context, AppDataNotifier notifier, child) {
         return const ViewSection().padding(vertical: 8);
       }),
     );
@@ -110,16 +83,19 @@ class MenuApp extends MenuItem {
 }
 
 class MenuAppSizes {
-  static double expandedIconSize = 16;
-  static double expandedIconPadding = 6;
+  static double iconSize = 16;
+  static double headerHeight = 26;
+  static double headerPadding = 6;
+  static double iconPadding = 6;
+  static double appVPadding = 14;
   static double scale = 1;
-  static double get expandedPadding => expandedIconSize * scale + expandedIconPadding;
+  static double get expandedPadding => iconSize * scale + headerPadding;
 }
 
-class ViewListNotifier extends ChangeNotifier {
+class AppDataNotifier extends ChangeNotifier {
   List<View> _views = [];
   View? _selectedView;
-  ViewListNotifier();
+  AppDataNotifier();
 
   set views(List<View>? items) {
     _views = items ?? List.empty(growable: false);
@@ -135,12 +111,3 @@ class ViewListNotifier extends ChangeNotifier {
 
   List<View> get views => _views;
 }
-
-class MenuAppContext {
-  final App app;
-  final viewList = ViewListNotifier();
-
-  MenuAppContext(this.app);
-
-  Key valueKey() => ValueKey("${app.id}${app.version}");
-}

+ 0 - 85
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/action.dart

@@ -1,85 +0,0 @@
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/hover.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:provider/provider.dart';
-import 'package:styled_widget/styled_widget.dart';
-import 'package:app_flowy/workspace/domain/view_edit.dart';
-
-class ViewActionList implements FlowyOverlayDelegate {
-  final Function(dartz.Option<ViewAction>) onSelected;
-  final BuildContext anchorContext;
-  final String _identifier = 'ViewActionList';
-
-  const ViewActionList({required this.anchorContext, required this.onSelected});
-
-  void show(BuildContext buildContext) {
-    final items = ViewAction.values
-        .map((action) => ActionItem(
-            action: action,
-            onSelected: (action) {
-              FlowyOverlay.of(buildContext).remove(_identifier);
-              onSelected(dartz.some(action));
-            }))
-        .toList();
-
-    ListOverlay.showWithAnchor(
-      buildContext,
-      identifier: _identifier,
-      itemCount: items.length,
-      itemBuilder: (context, index) => items[index],
-      anchorContext: anchorContext,
-      anchorDirection: AnchorDirection.bottomRight,
-      maxWidth: 162,
-      maxHeight: ViewAction.values.length * 32,
-      delegate: this,
-    );
-  }
-
-  @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-}
-
-class ActionItem extends StatelessWidget {
-  final ViewAction action;
-  final Function(ViewAction) onSelected;
-  const ActionItem({
-    Key? key,
-    required this.action,
-    required this.onSelected,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-
-    return FlowyHover(
-      config: HoverDisplayConfig(hoverColor: theme.hover),
-      builder: (context, onHover) {
-        return GestureDetector(
-          behavior: HitTestBehavior.opaque,
-          onTap: () => onSelected(action),
-          child: Row(
-            children: [
-              action.icon,
-              const HSpace(10),
-              FlowyText.medium(
-                action.name,
-                fontSize: 12,
-              ),
-            ],
-          ).padding(
-            horizontal: 6,
-            vertical: 6,
-          ),
-        );
-      },
-    );
-  }
-}

+ 72 - 0
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/disclosure_action.dart

@@ -0,0 +1,72 @@
+import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:dartz/dartz.dart' as dartz;
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/material.dart';
+
+// [[Widget: LifeCycle]]
+// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
+class ViewDisclosureButton extends StatelessWidget
+    with ActionList<ViewDisclosureActionWrapper>
+    implements FlowyOverlayDelegate {
+  final Function() onTap;
+  final Function(dartz.Option<ViewDisclosureAction>) onSelected;
+  final _items = ViewDisclosureAction.values.map((action) => ViewDisclosureActionWrapper(action)).toList();
+
+  ViewDisclosureButton({
+    Key? key,
+    required this.onTap,
+    required this.onSelected,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlowyIconButton(
+      iconPadding: const EdgeInsets.all(5),
+      width: 26,
+      onPressed: () {
+        onTap();
+        show(context, context);
+      },
+      icon: svg("editor/details"),
+    );
+  }
+
+  @override
+  String get identifier => "ViewDisclosureActions";
+
+  @override
+  List<ViewDisclosureActionWrapper> get items => _items;
+
+  @override
+  double get maxWidth => 162;
+
+  @override
+  void Function(dartz.Option<ViewDisclosureActionWrapper> p1) get selectCallback => (result) {
+        result.fold(
+          () => onSelected(dartz.none()),
+          (wrapper) => onSelected(dartz.some(wrapper.inner)),
+        );
+      };
+
+  @override
+  FlowyOverlayDelegate? get delegate => this;
+
+  @override
+  void didRemove() {
+    onSelected(dartz.none());
+  }
+}
+
+class ViewDisclosureActionWrapper extends ActionItemData {
+  final ViewDisclosureAction inner;
+
+  ViewDisclosureActionWrapper(this.inner);
+  @override
+  Widget get icon => inner.icon;
+
+  @override
+  String get name => inner.name;
+}

+ 7 - 41
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart

@@ -1,13 +1,10 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/view/view_bloc.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/domain/view_ext.dart';
+import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:dartz/dartz.dart' as dartz;
-import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
@@ -15,12 +12,10 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
-
 import 'package:app_flowy/workspace/domain/image.dart';
-import 'package:app_flowy/workspace/domain/view_edit.dart';
 import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
 
-import 'action.dart';
+import 'disclosure_action.dart';
 
 // ignore: must_be_immutable
 class ViewSectionItem extends StatelessWidget {
@@ -81,15 +76,15 @@ class ViewSectionItem extends StatelessWidget {
       height: 26,
       child: Row(children: children).padding(
         left: MenuAppSizes.expandedPadding,
-        right: MenuAppSizes.expandedIconPadding,
+        right: MenuAppSizes.headerPadding,
       ),
     );
   }
 
-  void _handleAction(BuildContext context, dartz.Option<ViewAction> action) {
+  void _handleAction(BuildContext context, dartz.Option<ViewDisclosureAction> action) {
     action.foldRight({}, (action, previous) {
       switch (action) {
-        case ViewAction.rename:
+        case ViewDisclosureAction.rename:
           TextFieldDialog(
             title: 'Rename',
             value: context.read<ViewBloc>().state.view.name,
@@ -99,42 +94,13 @@ class ViewSectionItem extends StatelessWidget {
           ).show(context);
 
           break;
-        case ViewAction.delete:
+        case ViewDisclosureAction.delete:
           context.read<ViewBloc>().add(const ViewEvent.delete());
           break;
-        case ViewAction.duplicate:
+        case ViewDisclosureAction.duplicate:
           context.read<ViewBloc>().add(const ViewEvent.duplicate());
           break;
       }
     });
   }
 }
-
-// [[Widget: LifeCycle]]
-// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
-
-class ViewDisclosureButton extends StatelessWidget {
-  final Function() onTap;
-  final Function(dartz.Option<ViewAction>) onSelected;
-  const ViewDisclosureButton({
-    Key? key,
-    required this.onTap,
-    required this.onSelected,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FlowyIconButton(
-      iconPadding: const EdgeInsets.all(5),
-      width: 26,
-      onPressed: () {
-        onTap();
-        ViewActionList(
-          anchorContext: context,
-          onSelected: onSelected,
-        ).show(context);
-      },
-      icon: svg("editor/details"),
-    );
-  }
-}

+ 11 - 11
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/section.dart

@@ -12,20 +12,20 @@ import 'item.dart';
 import 'package:async/async.dart';
 
 class ViewSectionNotifier with ChangeNotifier {
-  List<View> innerViews;
+  List<View> _views;
   View? _selectedView;
   CancelableOperation? _notifyListenerOperation;
-  ViewSectionNotifier(this.innerViews);
+  ViewSectionNotifier(List<View> views) : _views = views;
 
-  set views(List<View> views) => innerViews = views;
-  List<View> get views => innerViews;
-  set setViews(List<View> views) {
-    if (innerViews != views) {
-      innerViews = views;
+  set views(List<View> views) {
+    if (_views != views) {
+      _views = views;
       _notifyListeners();
     }
   }
 
+  List<View> get views => _views;
+
   set selectView(View? view) {
     if (_selectedView == view) {
       return;
@@ -48,8 +48,8 @@ class ViewSectionNotifier with ChangeNotifier {
 
   View? get selectedView => _selectedView;
 
-  void update(ViewListNotifier notifier) {
-    setViews = notifier.views;
+  void update(AppDataNotifier notifier) {
+    views = notifier.views;
     selectView = notifier.selectedView;
   }
 
@@ -69,9 +69,9 @@ class ViewSection extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     // The ViewListNotifier will be updated after ViewListData changed passed by parent widget
-    return ChangeNotifierProxyProvider<ViewListNotifier, ViewSectionNotifier>(
+    return ChangeNotifierProxyProvider<AppDataNotifier, ViewSectionNotifier>(
       create: (_) {
-        final views = Provider.of<ViewListNotifier>(context, listen: false).views;
+        final views = Provider.of<AppDataNotifier>(context, listen: false).views;
         return ViewSectionNotifier(views);
       },
       update: (_, notifier, controller) => controller!..update(notifier),

+ 99 - 0
app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -0,0 +1,99 @@
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:dartz/dartz.dart' as dartz;
+
+abstract class ActionList<T extends ActionItemData> {
+  List<T> get items;
+
+  String get identifier;
+
+  double get maxWidth;
+
+  void Function(dartz.Option<T>) get selectCallback;
+
+  FlowyOverlayDelegate? get delegate;
+
+  void show(BuildContext buildContext, BuildContext anchorContext,
+      {AnchorDirection anchorDirection = AnchorDirection.bottomRight}) {
+    final widgets = items
+        .map((action) => ActionItem<T>(
+            action: action,
+            onSelected: (action) {
+              FlowyOverlay.of(buildContext).remove(identifier);
+              selectCallback(dartz.some(action));
+            }))
+        .toList();
+
+    double totalHeight = widgets.length * (ActionListSizes.itemHeight + ActionListSizes.padding * 2);
+
+    ListOverlay.showWithAnchor(
+      buildContext,
+      identifier: identifier,
+      itemCount: widgets.length,
+      itemBuilder: (context, index) => widgets[index],
+      anchorContext: anchorContext,
+      anchorDirection: anchorDirection,
+      maxWidth: maxWidth,
+      maxHeight: totalHeight,
+      delegate: delegate,
+    );
+  }
+}
+
+abstract class ActionItemData {
+  Widget get icon;
+  String get name;
+}
+
+class ActionListSizes {
+  static double itemHPadding = 10;
+  static double itemHeight = 16;
+  static double padding = 6;
+}
+
+class ActionItem<T extends ActionItemData> extends StatelessWidget {
+  final T action;
+  final Function(T) onSelected;
+  const ActionItem({
+    Key? key,
+    required this.action,
+    required this.onSelected,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return FlowyHover(
+      config: HoverDisplayConfig(hoverColor: theme.hover),
+      builder: (context, onHover) {
+        return GestureDetector(
+          behavior: HitTestBehavior.opaque,
+          onTap: () => onSelected(action),
+          child: SizedBox(
+            height: ActionListSizes.itemHeight,
+            child: Row(
+              children: [
+                action.icon,
+                HSpace(ActionListSizes.itemHPadding),
+                FlowyText.medium(
+                  action.name,
+                  fontSize: 12,
+                ),
+              ],
+            ),
+          ).padding(
+            horizontal: ActionListSizes.padding,
+            vertical: ActionListSizes.padding,
+          ),
+        );
+      },
+    );
+  }
+}

+ 3 - 2
app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart

@@ -20,13 +20,14 @@ class ListOverlay extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
     return Material(
       type: MaterialType.transparency,
       child: Container(
-        constraints: BoxConstraints.tight(Size(maxWidth, maxHeight)),
+        constraints: BoxConstraints.tight(Size(maxWidth, maxHeight + padding.vertical)),
         decoration: FlowyDecoration.decoration(),
         child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
+          padding: padding,
           child: ListView.builder(
             shrinkWrap: true,
             itemBuilder: itemBuilder,

+ 1 - 1
rust-lib/flowy-document/src/services/doc/edit/doc_actor.rs

@@ -9,7 +9,7 @@ use crate::{
 use async_stream::stream;
 use flowy_ot::core::{Delta, OperationTransformable};
 use futures::stream::StreamExt;
-use std::{convert::TryFrom, sync::Arc, thread};
+use std::{convert::TryFrom, sync::Arc};
 use tokio::sync::{mpsc, RwLock};
 
 pub struct DocumentActor {

+ 12 - 6
rust-lib/flowy-workspace/src/services/view_controller.rs

@@ -164,6 +164,9 @@ impl ViewController {
             .payload(updated_view.clone())
             .send();
 
+        //
+        let _ = notify_view_num_changed(&updated_view.belong_to_id, self.trash_can.clone(), conn)?;
+
         let _ = self.update_view_on_server(params);
         Ok(updated_view)
     }
@@ -267,8 +270,7 @@ async fn handle_trash_event(
                 let _ = conn.immediate_transaction::<_, WorkspaceError, _>(|| {
                     for identifier in identifiers.items {
                         let view_table = ViewTableSql::read_view(&identifier.id, conn)?;
-                        let repeated_view = read_belonging_view(&view_table.belong_to_id, trash_can.clone(), conn)?;
-                        let _ = notify_view_num_changed(&view_table.belong_to_id, repeated_view)?;
+                        let _ = notify_view_num_changed(&view_table.belong_to_id, trash_can.clone(), conn)?;
                     }
                     Ok(())
                 })?;
@@ -289,8 +291,7 @@ async fn handle_trash_event(
                     }
 
                     for notify_id in notify_ids {
-                        let repeated_view = read_belonging_view(&notify_id, trash_can.clone(), conn)?;
-                        let _ = notify_view_num_changed(&notify_id, repeated_view)?;
+                        let _ = notify_view_num_changed(&notify_id, trash_can.clone(), conn)?;
                     }
 
                     Ok(())
@@ -302,8 +303,13 @@ async fn handle_trash_event(
     }
 }
 
-#[tracing::instrument(skip(repeated_view), fields(view_count), err)]
-fn notify_view_num_changed(belong_to_id: &str, repeated_view: RepeatedView) -> WorkspaceResult<()> {
+#[tracing::instrument(skip(belong_to_id, trash_can, conn), fields(view_count), err)]
+fn notify_view_num_changed(
+    belong_to_id: &str,
+    trash_can: Arc<TrashCan>,
+    conn: &SqliteConnection,
+) -> WorkspaceResult<()> {
+    let repeated_view = read_belonging_view(belong_to_id, trash_can.clone(), conn)?;
     tracing::Span::current().record("view_count", &format!("{}", repeated_view.len()).as_str());
     send_dart_notification(&belong_to_id, WorkspaceNotification::AppViewsChanged)
         .payload(repeated_view)