ソースを参照

Merge pull request #521 from AppFlowy-IO/fix/grid_ui_adjust

Fix: grid UI adjust
Nathan.fooo 2 年 前
コミット
a384404814
24 ファイル変更484 行追加386 行削除
  1. 2 1
      frontend/app_flowy/assets/translations/en.json
  2. 3 0
      frontend/app_flowy/lib/startup/deps_resolver.dart
  3. 35 33
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  4. 5 5
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
  5. 2 7
      frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart
  6. 37 0
      frontend/app_flowy/lib/workspace/presentation/home/toast.dart
  7. 171 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart
  8. 33 22
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart
  9. 5 5
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart
  10. 2 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart
  11. 13 15
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart
  12. 2 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart
  13. 11 11
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart
  14. 11 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart
  15. 90 31
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart
  16. 0 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart
  17. 28 17
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  18. 0 44
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/number_cell.dart
  19. 1 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart
  20. 19 14
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart
  21. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_setting.dart
  22. 9 51
      frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  23. 3 121
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart
  24. 1 1
      frontend/app_flowy/pubspec.yaml

+ 2 - 1
frontend/app_flowy/assets/translations/en.json

@@ -180,7 +180,8 @@
     "row": {
       "duplicate": "Duplicate",
       "delete": "Delete",
-      "textPlaceholder": "Empty"
+      "textPlaceholder": "Empty",
+      "copyProperty": "Copied property to clipboard"
     },
     "selectOption": {
       "create": "Create",

+ 3 - 0
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -16,6 +16,7 @@ import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:fluttertoast/fluttertoast.dart';
 import 'package:get_it/get_it.dart';
 
 class DependencyResolver {
@@ -46,6 +47,8 @@ void _resolveUserDeps(GetIt getIt) {
 }
 
 void _resolveHomeDeps(GetIt getIt) {
+  getIt.registerSingleton(FToast());
+
   getIt.registerSingleton(MenuSharedState());
 
   getIt.registerFactoryParam<UserListener, UserProfile, void>(

+ 35 - 33
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -67,40 +67,42 @@ class ApplicationWidget extends StatelessWidget {
   }) : super(key: key);
 
   @override
-  Widget build(BuildContext context) => ChangeNotifierProvider.value(
-        value: settingModel,
-        builder: (context, _) {
-          const ratio = 1.73;
-          const minWidth = 600.0;
-          setWindowMinSize(const Size(minWidth, minWidth / ratio));
-          settingModel.readLocaleWhenAppLaunch(context);
-          AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
-            (value) => value.theme,
-          );
-          Locale locale = context.select<AppearanceSettingModel, Locale>(
-            (value) => value.locale,
-          );
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: settingModel,
+      builder: (context, _) {
+        const ratio = 1.73;
+        const minWidth = 600.0;
+        setWindowMinSize(const Size(minWidth, minWidth / ratio));
+        settingModel.readLocaleWhenAppLaunch(context);
+        AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
+          (value) => value.theme,
+        );
+        Locale locale = context.select<AppearanceSettingModel, Locale>(
+          (value) => value.locale,
+        );
 
-          return MultiProvider(
-            providers: [
-              Provider.value(value: theme),
-              Provider.value(value: locale),
-            ],
-            builder: (context, _) {
-              return MaterialApp(
-                builder: overlayManagerBuilder(),
-                debugShowCheckedModeBanner: false,
-                theme: theme.themeData,
-                localizationsDelegates: context.localizationDelegates,
-                supportedLocales: context.supportedLocales,
-                locale: locale,
-                navigatorKey: AppGlobals.rootNavKey,
-                home: child,
-              );
-            },
-          );
-        },
-      );
+        return MultiProvider(
+          providers: [
+            Provider.value(value: theme),
+            Provider.value(value: locale),
+          ],
+          builder: (context, _) {
+            return MaterialApp(
+              builder: overlayManagerBuilder(),
+              debugShowCheckedModeBanner: false,
+              theme: theme.themeData,
+              localizationsDelegates: context.localizationDelegates,
+              supportedLocales: context.supportedLocales,
+              locale: locale,
+              navigatorKey: AppGlobals.rootNavKey,
+              home: child,
+            );
+          },
+        );
+      },
+    );
+  }
 }
 
 class AppGlobals {

+ 5 - 5
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart

@@ -105,7 +105,7 @@ class _GridCellContext<T, D> extends Equatable {
   final FieldService _fieldService;
 
   late final CellListener _cellListener;
-  late final ValueNotifier<T?> _cellDataNotifier;
+  late final ValueNotifier<T?>? _cellDataNotifier;
   bool isListening = false;
   VoidCallback? _onFieldChangedFn;
   Timer? _loadDataOperation;
@@ -163,19 +163,19 @@ class _GridCellContext<T, D> extends Equatable {
     }
 
     onCellChangedFn() {
-      onCellChanged(_cellDataNotifier.value);
+      onCellChanged(_cellDataNotifier?.value);
 
       if (cellDataLoader.config.reloadOnCellChanged) {
         _loadData();
       }
     }
 
-    _cellDataNotifier.addListener(onCellChangedFn);
+    _cellDataNotifier?.addListener(onCellChangedFn);
     return onCellChangedFn;
   }
 
   void removeListener(VoidCallback fn) {
-    _cellDataNotifier.removeListener(fn);
+    _cellDataNotifier?.removeListener(fn);
   }
 
   T? getCellData({bool loadIfNoCache = true}) {
@@ -211,7 +211,7 @@ class _GridCellContext<T, D> extends Equatable {
     _loadDataOperation?.cancel();
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       cellDataLoader.loadData().then((data) {
-        _cellDataNotifier.value = data;
+        _cellDataNotifier?.value = data;
         cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
       });
     });

+ 2 - 7
frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart

@@ -2,15 +2,13 @@ import 'dart:io' show Platform;
 
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
-import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
+import 'package:app_flowy/workspace/presentation/home/toast.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
 import 'package:time/time.dart';
-import 'package:fluttertoast/fluttertoast.dart';
-
 import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
 import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
@@ -22,8 +20,6 @@ import 'package:flowy_infra/notifier.dart';
 
 typedef NavigationCallback = void Function(String id);
 
-late FToast fToast;
-
 class HomeStack extends StatelessWidget {
   static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
   // final Size size;
@@ -74,8 +70,7 @@ class _FadingIndexedStackState extends State<FadingIndexedStack> {
   @override
   void initState() {
     super.initState();
-    fToast = FToast();
-    fToast.init(HomeScreen.scaffoldKey.currentState!.context);
+    initToastWithContext(context);
   }
 
   @override

+ 37 - 0
frontend/app_flowy/lib/workspace/presentation/home/toast.dart

@@ -0,0 +1,37 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+
+class FlowyMessageToast extends StatelessWidget {
+  final String message;
+  const FlowyMessageToast({required this.message, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+        child: FlowyText.medium(message, color: Colors.white),
+      ),
+      decoration: const BoxDecoration(
+        borderRadius: BorderRadius.all(Radius.circular(4)),
+        color: Colors.black,
+      ),
+    );
+  }
+}
+
+void initToastWithContext(BuildContext context) {
+  getIt<FToast>().init(context);
+}
+
+void showMessageToast(String message) {
+  final child = FlowyMessageToast(message: message);
+
+  getIt<FToast>().showToast(
+    child: child,
+    gravity: ToastGravity.BOTTOM,
+    toastDuration: const Duration(seconds: 3),
+  );
+}

+ 171 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart

@@ -0,0 +1,171 @@
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+class GridCellAccessoryBuildContext {
+  final BuildContext anchorContext;
+
+  GridCellAccessoryBuildContext({required this.anchorContext});
+}
+
+abstract class GridCellAccessory implements Widget {
+  void onTap();
+}
+
+typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
+
+abstract class AccessoryWidget extends Widget {
+  const AccessoryWidget({Key? key}) : super(key: key);
+
+  // The hover will show if the onFocus's value is true
+  ValueNotifier<bool>? get isFocus;
+
+  AccessoryBuilder? get accessoryBuilder;
+}
+
+class AccessoryHover extends StatefulWidget {
+  final AccessoryWidget child;
+  final EdgeInsets contentPadding;
+  const AccessoryHover({
+    required this.child,
+    this.contentPadding = EdgeInsets.zero,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<AccessoryHover> createState() => _AccessoryHoverState();
+}
+
+class _AccessoryHoverState extends State<AccessoryHover> {
+  late AccessoryHoverState _hoverState;
+  VoidCallback? _listenerFn;
+
+  @override
+  void initState() {
+    _hoverState = AccessoryHoverState();
+    _listenerFn = () => _hoverState.isFocus = widget.child.isFocus?.value ?? false;
+    widget.child.isFocus?.addListener(_listenerFn!);
+
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _hoverState.dispose();
+
+    if (_listenerFn != null) {
+      widget.child.isFocus?.removeListener(_listenerFn!);
+      _listenerFn = null;
+    }
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> children = [
+      const _Background(),
+      Padding(padding: widget.contentPadding, child: widget.child),
+    ];
+
+    final accessoryBuilder = widget.child.accessoryBuilder;
+    if (accessoryBuilder != null) {
+      final accessories = accessoryBuilder((GridCellAccessoryBuildContext(anchorContext: context)));
+      children.add(
+        Padding(
+          padding: const EdgeInsets.only(right: 6),
+          child: AccessoryContainer(accessories: accessories),
+        ).positioned(right: 0),
+      );
+    }
+
+    return ChangeNotifierProvider.value(
+      value: _hoverState,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        opaque: false,
+        onEnter: (p) => setState(() => _hoverState.onHover = true),
+        onExit: (p) => setState(() => _hoverState.onHover = false),
+        child: Stack(
+          fit: StackFit.loose,
+          alignment: AlignmentDirectional.center,
+          children: children,
+        ),
+      ),
+    );
+  }
+}
+
+class AccessoryHoverState extends ChangeNotifier {
+  bool _onHover = false;
+  bool _isFocus = false;
+
+  set onHover(bool value) {
+    if (_onHover != value) {
+      _onHover = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onHover => _onHover;
+
+  set isFocus(bool value) {
+    if (_isFocus != value) {
+      _isFocus = value;
+      notifyListeners();
+    }
+  }
+
+  bool get isFocus => _isFocus;
+}
+
+class _Background extends StatelessWidget {
+  const _Background({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return Consumer<AccessoryHoverState>(
+      builder: (context, state, child) {
+        if (state.onHover || state.isFocus) {
+          return FlowyHoverContainer(
+            style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
+          );
+        } else {
+          return const SizedBox();
+        }
+      },
+    );
+  }
+}
+
+class AccessoryContainer extends StatelessWidget {
+  final List<GridCellAccessory> accessories;
+  const AccessoryContainer({required this.accessories, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    final children = accessories.map((accessory) {
+      final hover = FlowyHover(
+        style: HoverStyle(hoverColor: theme.bg3, backgroundColor: theme.surface),
+        builder: (_, onHover) => Container(
+          width: 26,
+          height: 26,
+          padding: const EdgeInsets.all(3),
+          child: accessory,
+        ),
+      );
+      return GestureDetector(
+        child: hover,
+        behavior: HitTestBehavior.opaque,
+        onTap: () => accessory.onTap(),
+      );
+    }).toList();
+
+    return Wrap(children: children, spacing: 6);
+  }
+}

+ 33 - 22
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
-import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
 import 'package:flutter/widgets.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
@@ -8,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:styled_widget/styled_widget.dart';
+import 'cell_accessory.dart';
 import 'checkbox_cell.dart';
 import 'date_cell/date_cell.dart';
 import 'number_cell.dart';
@@ -48,24 +48,27 @@ class BlankCell extends StatelessWidget {
   }
 }
 
-abstract class GridCellWidget implements FlowyHoverWidget {
+abstract class GridCellWidget implements AccessoryWidget, CellContainerFocustable {
   @override
-  final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false);
+  final ValueNotifier<bool> isFocus = ValueNotifier<bool>(false);
 
-  final GridCellRequestFocusNotifier requestFocus = GridCellRequestFocusNotifier();
+  @override
+  List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
+
+  @override
+  final GridCellRequestBeginFocus requestBeginFocus = GridCellRequestBeginFocus();
 }
 
-class GridCellRequestFocusNotifier extends ChangeNotifier {
+class GridCellRequestBeginFocus extends ChangeNotifier {
   VoidCallback? _listener;
 
-  @override
-  void addListener(VoidCallback listener) {
+  void setListener(VoidCallback listener) {
     if (_listener != null) {
       removeListener(_listener!);
     }
 
     _listener = listener;
-    super.addListener(listener);
+    addListener(listener);
   }
 
   void removeAllListener() {
@@ -81,10 +84,10 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
 
 abstract class GridCellStyle {}
 
-class CellSingleFocusNode extends FocusNode {
+class SingleListenrFocusNode extends FocusNode {
   VoidCallback? _listener;
 
-  void setSingleListener(VoidCallback listener) {
+  void setListener(VoidCallback listener) {
     if (_listener != null) {
       removeListener(_listener!);
     }
@@ -93,7 +96,7 @@ class CellSingleFocusNode extends FocusNode {
     super.addListener(listener);
   }
 
-  void removeSingleListener() {
+  void removeAllListener() {
     if (_listener != null) {
       removeListener(_listener!);
     }
@@ -123,9 +126,14 @@ class CellStateNotifier extends ChangeNotifier {
   bool get onEnter => _onEnter;
 }
 
+abstract class CellContainerFocustable {
+  // Listen on the requestBeginFocus if the
+  GridCellRequestBeginFocus get requestBeginFocus;
+}
+
 class CellContainer extends StatelessWidget {
   final GridCellWidget child;
-  final Widget? expander;
+  final AccessoryBuilder? accessoryBuilder;
   final double width;
   final RegionStateNotifier rowStateNotifier;
   const CellContainer({
@@ -133,7 +141,7 @@ class CellContainer extends StatelessWidget {
     required this.child,
     required this.width,
     required this.rowStateNotifier,
-    this.expander,
+    this.accessoryBuilder,
   }) : super(key: key);
 
   @override
@@ -145,17 +153,21 @@ class CellContainer extends StatelessWidget {
         selector: (context, notifier) => notifier.isFocus,
         builder: (context, isFocus, _) {
           Widget container = Center(child: child);
-          child.onFocus.addListener(() {
-            Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
+          child.isFocus.addListener(() {
+            Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.isFocus.value;
           });
 
-          if (expander != null) {
-            container = CellEnterRegion(child: container, expander: expander!);
+          if (accessoryBuilder != null) {
+            final buildContext = GridCellAccessoryBuildContext(anchorContext: context);
+            final accessories = accessoryBuilder!(buildContext);
+            if (accessories.isNotEmpty) {
+              container = CellEnterRegion(child: container, accessories: accessories);
+            }
           }
 
           return GestureDetector(
             behavior: HitTestBehavior.translucent,
-            onTap: () => child.requestFocus.notify(),
+            onTap: () => child.requestBeginFocus.notify(),
             child: Container(
               constraints: BoxConstraints(maxWidth: width, minHeight: 46),
               decoration: _makeBoxDecoration(context, isFocus),
@@ -182,8 +194,8 @@ class CellContainer extends StatelessWidget {
 
 class CellEnterRegion extends StatelessWidget {
   final Widget child;
-  final Widget expander;
-  const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
+  final List<GridCellAccessory> accessories;
+  const CellEnterRegion({required this.child, required this.accessories, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -192,7 +204,7 @@ class CellEnterRegion extends StatelessWidget {
       builder: (context, onEnter, _) {
         List<Widget> children = [child];
         if (onEnter) {
-          children.add(expander.positioned(right: 0));
+          children.add(AccessoryContainer(accessories: accessories).positioned(right: 0));
         }
 
         return MouseRegion(
@@ -202,7 +214,6 @@ class CellEnterRegion extends StatelessWidget {
           child: Stack(
             alignment: AlignmentDirectional.center,
             fit: StackFit.expand,
-            // alignment: AlignmentDirectional.centerEnd,
             children: children,
           ),
         );

+ 5 - 5
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart

@@ -24,7 +24,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
   void initState() {
     final cellContext = widget.cellContextBuilder.build();
     _cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
-    _listenCellRequestFocus();
+    _handleRequestFocus();
     super.initState();
   }
 
@@ -51,19 +51,19 @@ class _CheckboxCellState extends State<CheckboxCell> {
 
   @override
   void didUpdateWidget(covariant CheckboxCell oldWidget) {
-    _listenCellRequestFocus();
+    _handleRequestFocus();
     super.didUpdateWidget(oldWidget);
   }
 
   @override
   Future<void> dispose() async {
-    widget.requestFocus.removeAllListener();
+    widget.requestBeginFocus.removeAllListener();
     _cellBloc.close();
     super.dispose();
   }
 
-  void _listenCellRequestFocus() {
-    widget.requestFocus.addListener(() {
+  void _handleRequestFocus() {
+    widget.requestBeginFocus.setListener(() {
       _cellBloc.add(const CheckboxCellEvent.select());
     });
   }

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart

@@ -76,8 +76,8 @@ class _DateCellState extends State<DateCell> {
 
   void _showCalendar(BuildContext context) {
     final bloc = context.read<DateCellBloc>();
-    widget.onFocus.value = true;
-    final calendar = DateCellEditor(onDismissed: () => widget.onFocus.value = false);
+    widget.isFocus.value = true;
+    final calendar = DateCellEditor(onDismissed: () => widget.isFocus.value = false);
     calendar.show(
       context,
       cellContext: bloc.cellContext.clone(),

+ 13 - 15
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart

@@ -22,7 +22,7 @@ class NumberCell extends StatefulWidget with GridCellWidget {
 class _NumberCellState extends State<NumberCell> {
   late NumberCellBloc _cellBloc;
   late TextEditingController _controller;
-  late CellSingleFocusNode _focusNode;
+  late SingleListenrFocusNode _focusNode;
   Timer? _delayOperation;
 
   @override
@@ -30,14 +30,14 @@ class _NumberCellState extends State<NumberCell> {
     final cellContext = widget.cellContextBuilder.build();
     _cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
     _controller = TextEditingController(text: contentFromState(_cellBloc.state));
-    _focusNode = CellSingleFocusNode();
-    _listenFocusNode();
+    _focusNode = SingleListenrFocusNode();
+    _listenOnFocusNodeChanged();
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    _listenCellRequestFocus(context);
+    _handleCellRequestFocus(context);
     return BlocProvider.value(
       value: _cellBloc,
       child: MultiBlocListener(
@@ -65,19 +65,17 @@ class _NumberCellState extends State<NumberCell> {
 
   @override
   Future<void> dispose() async {
-    widget.requestFocus.removeAllListener();
+    widget.requestBeginFocus.removeAllListener();
     _delayOperation?.cancel();
     _cellBloc.close();
-    _focusNode.removeSingleListener();
+    _focusNode.removeAllListener();
     _focusNode.dispose();
     super.dispose();
   }
 
   @override
   void didUpdateWidget(covariant NumberCell oldWidget) {
-    if (oldWidget != widget) {
-      _listenFocusNode();
-    }
+    _listenOnFocusNodeChanged();
     super.didUpdateWidget(oldWidget);
   }
 
@@ -92,16 +90,16 @@ class _NumberCellState extends State<NumberCell> {
     }
   }
 
-  void _listenFocusNode() {
-    widget.onFocus.value = _focusNode.hasFocus;
-    _focusNode.setSingleListener(() {
-      widget.onFocus.value = _focusNode.hasFocus;
+  void _listenOnFocusNodeChanged() {
+    widget.isFocus.value = _focusNode.hasFocus;
+    _focusNode.setListener(() {
+      widget.isFocus.value = _focusNode.hasFocus;
       focusChanged();
     });
   }
 
-  void _listenCellRequestFocus(BuildContext context) {
-    widget.requestFocus.addListener(() {
+  void _handleCellRequestFocus(BuildContext context) {
+    widget.requestBeginFocus.setListener(() {
       if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
         FocusScope.of(context).requestFocus(_focusNode);
       }

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart

@@ -59,7 +59,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
           return _SelectOptionCell(
               selectOptions: state.selectedOptions,
               cellStyle: widget.cellStyle,
-              onFocus: (value) => widget.onFocus.value = value,
+              onFocus: (value) => widget.isFocus.value = value,
               cellContextBuilder: widget.cellContextBuilder);
         },
       ),
@@ -113,7 +113,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
           return _SelectOptionCell(
               selectOptions: state.selectedOptions,
               cellStyle: widget.cellStyle,
-              onFocus: (value) => widget.onFocus.value = value,
+              onFocus: (value) => widget.isFocus.value = value,
               cellContextBuilder: widget.cellContextBuilder);
         },
       ),

+ 11 - 11
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

@@ -35,7 +35,7 @@ class GridTextCell extends StatefulWidget with GridCellWidget {
 class _GridTextCellState extends State<GridTextCell> {
   late TextCellBloc _cellBloc;
   late TextEditingController _controller;
-  late CellSingleFocusNode _focusNode;
+  late SingleListenrFocusNode _focusNode;
   Timer? _delayOperation;
 
   @override
@@ -44,9 +44,9 @@ class _GridTextCellState extends State<GridTextCell> {
     _cellBloc = getIt<TextCellBloc>(param1: cellContext);
     _cellBloc.add(const TextCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = CellSingleFocusNode();
+    _focusNode = SingleListenrFocusNode();
 
-    _listenFocusNode();
+    _listenOnFocusNodeChanged();
     _listenRequestFocus(context);
     super.initState();
   }
@@ -81,10 +81,10 @@ class _GridTextCellState extends State<GridTextCell> {
 
   @override
   Future<void> dispose() async {
-    widget.requestFocus.removeAllListener();
+    widget.requestBeginFocus.removeAllListener();
     _delayOperation?.cancel();
     _cellBloc.close();
-    _focusNode.removeSingleListener();
+    _focusNode.removeAllListener();
     _focusNode.dispose();
 
     super.dispose();
@@ -93,21 +93,21 @@ class _GridTextCellState extends State<GridTextCell> {
   @override
   void didUpdateWidget(covariant GridTextCell oldWidget) {
     if (oldWidget != widget) {
-      _listenFocusNode();
+      _listenOnFocusNodeChanged();
     }
     super.didUpdateWidget(oldWidget);
   }
 
-  void _listenFocusNode() {
-    widget.onFocus.value = _focusNode.hasFocus;
-    _focusNode.setSingleListener(() {
-      widget.onFocus.value = _focusNode.hasFocus;
+  void _listenOnFocusNodeChanged() {
+    widget.isFocus.value = _focusNode.hasFocus;
+    _focusNode.setListener(() {
+      widget.isFocus.value = _focusNode.hasFocus;
       focusChanged();
     });
   }
 
   void _listenRequestFocus(BuildContext context) {
-    widget.requestFocus.addListener(() {
+    widget.requestBeginFocus.setListener(() {
       if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
         FocusScope.of(context).requestFocus(_focusNode);
       }

+ 11 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart

@@ -6,7 +6,7 @@ import 'dart:async';
 
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class URLCellEditor extends StatefulWidget {
+class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
   final GridURLCellContext cellContext;
   const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
 
@@ -25,18 +25,27 @@ class URLCellEditor extends StatefulWidget {
     //
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
-        child: SizedBox(width: 200, child: editor),
+        child: SizedBox(
+          width: 200,
+          child: Padding(padding: const EdgeInsets.all(6), child: editor),
+        ),
         constraints: BoxConstraints.loose(const Size(300, 160)),
       ),
       identifier: URLCellEditor.identifier(),
       anchorContext: context,
       anchorDirection: AnchorDirection.bottomWithCenterAligned,
+      delegate: editor,
     );
   }
 
   static String identifier() {
     return (URLCellEditor).toString();
   }
+
+  @override
+  bool asBarrier() {
+    return true;
+  }
 }
 
 class _URLCellEditorState extends State<URLCellEditor> {

+ 90 - 31
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart

@@ -1,10 +1,13 @@
 import 'dart:async';
+import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
+import 'package:app_flowy/workspace/presentation/home/toast.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
-import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:url_launcher/url_launcher.dart';
@@ -14,11 +17,19 @@ import 'cell_editor.dart';
 class GridURLCellStyle extends GridCellStyle {
   String? placeholder;
 
+  List<GridURLCellAccessoryType> accessoryTypes;
+
   GridURLCellStyle({
     this.placeholder,
+    this.accessoryTypes = const [],
   });
 }
 
+enum GridURLCellAccessoryType {
+  edit,
+  copyURL,
+}
+
 class GridURLCell extends StatefulWidget with GridCellWidget {
   final GridCellContextBuilder cellContextBuilder;
   late final GridURLCellStyle? cellStyle;
@@ -36,6 +47,35 @@ class GridURLCell extends StatefulWidget with GridCellWidget {
 
   @override
   State<GridURLCell> createState() => _GridURLCellState();
+
+  GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
+    switch (ty) {
+      case GridURLCellAccessoryType.edit:
+        final cellContext = cellContextBuilder.build() as GridURLCellContext;
+        return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext);
+
+      case GridURLCellAccessoryType.copyURL:
+        final cellContext = cellContextBuilder.build() as GridURLCellContext;
+        return _CopyURLAccessory(cellContext: cellContext);
+    }
+  }
+
+  @override
+  List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext) get accessoryBuilder => (buildContext) {
+        final List<GridCellAccessory> accessories = [];
+        if (cellStyle != null) {
+          accessories.addAll(cellStyle!.accessoryTypes.map((ty) {
+            return accessoryFromType(ty, buildContext);
+          }));
+        }
+
+        // If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
+        if (accessories.isEmpty) {
+          accessories.add(accessoryFromType(GridURLCellAccessoryType.edit, buildContext));
+        }
+
+        return accessories;
+      };
 }
 
 class _GridURLCellState extends State<GridURLCell> {
@@ -46,7 +86,7 @@ class _GridURLCellState extends State<GridURLCell> {
     final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
     _cellBloc = URLCellBloc(cellContext: cellContext);
     _cellBloc.add(const URLCellEvent.initial());
-    _listenRequestFocus(context);
+    _handleRequestFocus();
     super.initState();
   }
 
@@ -66,17 +106,18 @@ class _GridURLCellState extends State<GridURLCell> {
                 fontSize: 14,
                 decoration: TextDecoration.underline,
               ),
-              recognizer: _tapGesture(context),
             ),
           );
 
-          return CellEnterRegion(
+          return SizedBox.expand(
+              child: GestureDetector(
             child: Align(alignment: Alignment.centerLeft, child: richText),
-            expander: _EditCellIndicator(onTap: () {
-              final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
-              URLCellEditor.show(context, cellContext);
-            }),
-          );
+            onTap: () async {
+              widget.isFocus.value = true;
+              final url = context.read<URLCellBloc>().state.url;
+              await _openUrlOrEdit(url);
+            },
+          ));
         },
       ),
     );
@@ -84,18 +125,15 @@ class _GridURLCellState extends State<GridURLCell> {
 
   @override
   Future<void> dispose() async {
-    widget.requestFocus.removeAllListener();
+    widget.requestBeginFocus.removeAllListener();
     _cellBloc.close();
     super.dispose();
   }
 
-  TapGestureRecognizer _tapGesture(BuildContext context) {
-    final gesture = TapGestureRecognizer();
-    gesture.onTap = () async {
-      final url = context.read<URLCellBloc>().state.url;
-      await _openUrlOrEdit(url);
-    };
-    return gesture;
+  @override
+  void didUpdateWidget(covariant GridURLCell oldWidget) {
+    _handleRequestFocus();
+    super.didUpdateWidget(oldWidget);
   }
 
   Future<void> _openUrlOrEdit(String url) async {
@@ -108,27 +146,48 @@ class _GridURLCellState extends State<GridURLCell> {
     }
   }
 
-  void _listenRequestFocus(BuildContext context) {
-    widget.requestFocus.addListener(() {
+  void _handleRequestFocus() {
+    widget.requestBeginFocus.setListener(() {
       _openUrlOrEdit(_cellBloc.state.url);
     });
   }
 }
 
-class _EditCellIndicator extends StatelessWidget {
-  final VoidCallback onTap;
-  const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key);
+class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
+  final GridURLCellContext cellContext;
+  final BuildContext anchorContext;
+  const _EditURLAccessory({
+    required this.cellContext,
+    required this.anchorContext,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return svgWidget("editor/edit", color: theme.iconColor);
+  }
+
+  @override
+  void onTap() {
+    URLCellEditor.show(anchorContext, cellContext);
+  }
+}
+
+class _CopyURLAccessory extends StatelessWidget with GridCellAccessory {
+  final GridURLCellContext cellContext;
+  const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return FlowyIconButton(
-      width: 26,
-      onPressed: onTap,
-      hoverColor: theme.hover,
-      radius: BorderRadius.circular(4),
-      iconPadding: const EdgeInsets.all(5),
-      icon: svgWidget("editor/edit", color: theme.iconColor),
-    );
+    return svgWidget("editor/copy", color: theme.iconColor);
+  }
+
+  @override
+  void onTap() {
+    final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? "";
+    Clipboard.setData(ClipboardData(text: content));
+    showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
   }
 }

+ 0 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart

@@ -86,7 +86,6 @@ class _GridHeaderCellContainer extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     final borderSide = BorderSide(color: theme.shader5, width: 1.0);
-
     final decoration = BoxDecoration(
         border: Border(
       top: borderSide,

+ 28 - 17
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -170,16 +171,30 @@ class _RowCells extends StatelessWidget {
   List<Widget> _makeCells(BuildContext context, GridCellMap gridCellMap) {
     return gridCellMap.values.map(
       (gridCell) {
-        Widget? expander;
+        final GridCellWidget child = buildGridCellWidget(gridCell, cellCache);
+        List<GridCellAccessory> accessories = [];
         if (gridCell.field.isPrimary) {
-          expander = _CellExpander(onExpand: onExpand);
+          accessories.add(_PrimaryCellAccessory(onTapCallback: onExpand));
+        }
+
+        accessoryBuilder(buildContext) {
+          final builder = child.accessoryBuilder;
+          List<GridCellAccessory> accessories = [];
+          if (gridCell.field.isPrimary) {
+            accessories.add(_PrimaryCellAccessory(onTapCallback: onExpand));
+          }
+
+          if (builder != null) {
+            accessories.addAll(builder(buildContext));
+          }
+          return accessories;
         }
 
         return CellContainer(
           width: gridCell.field.width.toDouble(),
-          child: buildGridCellWidget(gridCell, cellCache),
+          child: child,
           rowStateNotifier: Provider.of<RegionStateNotifier>(context, listen: false),
-          expander: expander,
+          accessoryBuilder: accessoryBuilder,
         );
       },
     ).toList();
@@ -199,23 +214,19 @@ class RegionStateNotifier extends ChangeNotifier {
   bool get onEnter => _onEnter;
 }
 
-class _CellExpander extends StatelessWidget {
-  final VoidCallback onExpand;
-  const _CellExpander({required this.onExpand, Key? key}) : super(key: key);
+class _PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
+  final VoidCallback onTapCallback;
+  const _PrimaryCellAccessory({required this.onTapCallback, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return FittedBox(
-      fit: BoxFit.contain,
-      child: FlowyIconButton(
-        width: 26,
-        onPressed: onExpand,
-        iconPadding: const EdgeInsets.all(5),
-        radius: BorderRadius.circular(4),
-        icon: svgWidget("grid/expander", color: theme.main1),
-      ),
-    );
+    return svgWidget("grid/expander", color: theme.main1);
+  }
+
+  @override
+  void onTap() {
+    onTapCallback();
   }
 }
 

+ 0 - 44
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/number_cell.dart

@@ -1,44 +0,0 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/grid/prelude.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-class NumberCell extends StatefulWidget {
-  final GridCell cellData;
-
-  const NumberCell({
-    required this.cellData,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<NumberCell> createState() => _NumberCellState();
-}
-
-class _NumberCellState extends State<NumberCell> {
-  late NumberCellBloc _cellBloc;
-
-  @override
-  void initState() {
-    _cellBloc = getIt<NumberCellBloc>(param1: widget.cellData);
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider.value(
-      value: _cellBloc,
-      child: BlocBuilder<NumberCellBloc, NumberCellState>(
-        builder: (context, state) {
-          return Container();
-        },
-      ),
-    );
-  }
-
-  @override
-  Future<void> dispose() async {
-    _cellBloc.close();
-    super.dispose();
-  }
-}

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart

@@ -24,6 +24,7 @@ class GridRowActionSheet extends StatelessWidget {
       child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
         builder: (context, state) {
           final cells = _RowAction.values
+              .where((value) => value.enable())
               .map(
                 (action) => _RowActionCell(
                   action: action,

+ 19 - 14
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart

@@ -3,6 +3,7 @@ import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
@@ -10,7 +11,6 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header
 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/scrolling/styled_scroll_bar.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -149,12 +149,18 @@ class _RowDetailCell extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-
-    final cell = buildGridCellWidget(
-      gridCell,
-      cellCache,
-      style: _buildCellStyle(theme, gridCell.field.fieldType),
+    final style = _customCellStyle(theme, gridCell.field.fieldType);
+    final cell = buildGridCellWidget(gridCell, cellCache, style: style);
+
+    final gesture = GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: () => cell.requestBeginFocus.notify(),
+      child: AccessoryHover(
+        child: cell,
+        contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
+      ),
     );
+
     return ConstrainedBox(
       constraints: const BoxConstraints(minHeight: 40),
       child: IntrinsicHeight(
@@ -167,12 +173,7 @@ class _RowDetailCell extends StatelessWidget {
               child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)),
             ),
             const HSpace(10),
-            Expanded(
-              child: FlowyHover2(
-                child: cell,
-                contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
-              ),
-            ),
+            Expanded(child: gesture),
           ],
         ),
       ),
@@ -191,7 +192,7 @@ class _RowDetailCell extends StatelessWidget {
   }
 }
 
-GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
+GridCellStyle? _customCellStyle(AppTheme theme, FieldType fieldType) {
   switch (fieldType) {
     case FieldType.Checkbox:
       return null;
@@ -217,7 +218,11 @@ GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
     case FieldType.URL:
       return GridURLCellStyle(
         placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+        accessoryTypes: [
+          GridURLCellAccessoryType.edit,
+          GridURLCellAccessoryType.copyURL,
+        ],
       );
   }
-  return null;
+  throw UnimplementedError;
 }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_setting.dart

@@ -85,7 +85,7 @@ class GridSettingList extends StatelessWidget {
   }
 
   Widget _renderList() {
-    final cells = GridSettingAction.values.map((action) {
+    final cells = GridSettingAction.values.where((value) => value.enable()).map((action) {
       return _SettingItem(action: action);
     }).toList();
 

+ 9 - 51
frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -1,5 +1,5 @@
 import 'package:app_flowy/startup/tasks/rust_sdk.dart';
-import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/toast.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
@@ -16,7 +16,6 @@ import 'package:package_info_plus/package_info_plus.dart';
 import 'package:url_launcher/url_launcher.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:device_info_plus/device_info_plus.dart';
-import 'package:fluttertoast/fluttertoast.dart';
 
 class QuestionBubble extends StatelessWidget {
   const QuestionBubble({Key? key}) : super(key: key);
@@ -46,7 +45,7 @@ class QuestionBubble extends StatelessWidget {
                   _launchURL("https://discord.gg/9Q2xaN37tV");
                   break;
                 case BubbleAction.debug:
-                  const _DebugToast().show();
+                  _DebugToast().show();
                   break;
               }
             });
@@ -71,55 +70,14 @@ class QuestionBubble extends StatelessWidget {
   }
 }
 
-class _DebugToast extends StatelessWidget {
-  const _DebugToast({Key? key}) : super(key: key);
+class _DebugToast {
+  void show() async {
+    var debugInfo = "";
+    debugInfo += await _getDeviceInfo();
+    debugInfo += await _getDocumentPath();
+    Clipboard.setData(ClipboardData(text: debugInfo));
 
-  @override
-  Widget build(BuildContext context) {
-    return FutureBuilder(
-      future: Future(() async {
-        var debugInfo = "";
-        debugInfo += await _getDeviceInfo();
-        debugInfo += await _getDocumentPath();
-
-        Clipboard.setData(ClipboardData(text: debugInfo));
-      }),
-      builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
-        if (snapshot.connectionState == ConnectionState.done) {
-          if (snapshot.hasError) {
-            return _done(context, Text("Error: ${snapshot.error}"));
-          } else {
-            return _done(context, null);
-          }
-        } else {
-          return const CircularProgressIndicator();
-        }
-      },
-    );
-  }
-
-  Widget _done(BuildContext context, Widget? error) {
-    final theme = context.watch<AppTheme>();
-    return Container(
-      padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
-      decoration: BoxDecoration(borderRadius: BorderRadius.circular(25.0), color: theme.main1),
-      child: Row(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          const Icon(Icons.check),
-          const SizedBox(width: 12.0),
-          (error == null) ? Text(LocaleKeys.questionBubble_debug_success.tr()) : error
-        ],
-      ),
-    );
-  }
-
-  void show() {
-    fToast.showToast(
-      child: this,
-      gravity: ToastGravity.BOTTOM,
-      toastDuration: const Duration(seconds: 3),
-    );
+    showMessageToast(LocaleKeys.questionBubble_debug_success.tr());
   }
 
   Future<String> _getDeviceInfo() async {

+ 3 - 121
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart

@@ -1,9 +1,6 @@
 import 'package:flutter/material.dart';
 // ignore: unused_import
 import 'package:flowy_infra/time/duration.dart';
-import 'package:flowy_infra/size.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:provider/provider.dart';
 
 typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
 
@@ -52,7 +49,7 @@ class _FlowyHoverState extends State<FlowyHover> {
         child: child,
       );
     } else {
-      return child;
+      return Container(child: child, color: widget.style.backgroundColor);
     }
   }
 }
@@ -63,12 +60,14 @@ class HoverStyle {
   final Color hoverColor;
   final BorderRadius borderRadius;
   final EdgeInsets contentMargin;
+  final Color backgroundColor;
 
   const HoverStyle(
       {this.borderColor = Colors.transparent,
       this.borderWidth = 0,
       this.borderRadius = const BorderRadius.all(Radius.circular(6)),
       this.contentMargin = EdgeInsets.zero,
+      this.backgroundColor = Colors.transparent,
       required this.hoverColor});
 }
 
@@ -100,120 +99,3 @@ class FlowyHoverContainer extends StatelessWidget {
     );
   }
 }
-
-//
-abstract class FlowyHoverWidget extends Widget {
-  const FlowyHoverWidget({Key? key}) : super(key: key);
-
-  ValueNotifier<bool>? get onFocus;
-}
-
-class FlowyHover2 extends StatefulWidget {
-  final FlowyHoverWidget child;
-  final EdgeInsets contentPadding;
-  const FlowyHover2({
-    required this.child,
-    this.contentPadding = EdgeInsets.zero,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<FlowyHover2> createState() => _FlowyHover2State();
-}
-
-class _FlowyHover2State extends State<FlowyHover2> {
-  late FlowyHoverState _hoverState;
-  VoidCallback? _listenerFn;
-
-  @override
-  void initState() {
-    _hoverState = FlowyHoverState();
-
-    listener() {
-      _hoverState.onFocus = widget.child.onFocus?.value ?? false;
-    }
-
-    _listenerFn = listener;
-    widget.child.onFocus?.addListener(listener);
-
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    _hoverState.dispose();
-
-    if (_listenerFn != null) {
-      widget.child.onFocus?.removeListener(_listenerFn!);
-      _listenerFn = null;
-    }
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProvider.value(
-      value: _hoverState,
-      child: MouseRegion(
-        cursor: SystemMouseCursors.click,
-        opaque: false,
-        onEnter: (p) => setState(() => _hoverState.onHover = true),
-        onExit: (p) => setState(() => _hoverState.onHover = false),
-        child: Stack(
-          fit: StackFit.loose,
-          alignment: AlignmentDirectional.center,
-          children: [
-            const _HoverBackground(),
-            Padding(
-              padding: widget.contentPadding,
-              child: widget.child,
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
-
-class _HoverBackground extends StatelessWidget {
-  const _HoverBackground({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return Consumer<FlowyHoverState>(
-      builder: (context, state, child) {
-        if (state.onHover || state.onFocus) {
-          return FlowyHoverContainer(
-            style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
-          );
-        } else {
-          return const SizedBox();
-        }
-      },
-    );
-  }
-}
-
-class FlowyHoverState extends ChangeNotifier {
-  bool _onHover = false;
-  bool _onFocus = false;
-
-  set onHover(bool value) {
-    if (_onHover != value) {
-      _onHover = value;
-      notifyListeners();
-    }
-  }
-
-  bool get onHover => _onHover;
-
-  set onFocus(bool value) {
-    if (_onFocus != value) {
-      _onFocus = value;
-      notifyListeners();
-    }
-  }
-
-  bool get onFocus => _onFocus;
-}

+ 1 - 1
frontend/app_flowy/pubspec.yaml

@@ -72,7 +72,7 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2
   device_info_plus: ^3.2.1
-  fluttertoast: ^8.0.8
+  fluttertoast: ^8.0.9
   table_calendar: ^3.0.5
   reorderables: ^0.5.0
   linked_scroll_controller: ^0.2.0