diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index 4659c98b55..bba172c27e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -11,7 +11,7 @@ void main() { const emoji = '😁'; - group('Icon', () { + group('Icon:', () { testWidgets('Update page icon in sidebar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -52,6 +52,7 @@ void main() { if (value == ViewLayoutPB.Chat) { continue; } + await tester.createNewPageWithNameUnderParent( name: value.name, parentName: gettingStarted, diff --git a/frontend/appflowy_flutter/lib/main.dart b/frontend/appflowy_flutter/lib/main.dart index 9f140489c4..9117acfd1b 100644 --- a/frontend/appflowy_flutter/lib/main.dart +++ b/frontend/appflowy_flutter/lib/main.dart @@ -3,7 +3,9 @@ import 'package:scaled_app/scaled_app.dart'; import 'startup/startup.dart'; Future main() async { - ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0); + ScaledWidgetsFlutterBinding.ensureInitialized( + scaleFactor: (_) => 1.0, + ); await runAppFlowy(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart index 7e26b34765..a8676a5e46 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart @@ -629,23 +629,23 @@ class FieldNameTextField extends StatefulWidget { } class _FieldNameTextFieldState extends State { - FocusNode focusNode = FocusNode(); + final focusNode = FocusNode(); @override void initState() { super.initState(); - focusNode.addListener(() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - }); + focusNode.addListener(_onFocusChanged); + widget.popoverMutex?.addPopoverListener(_onPopoverChanged); + } - widget.popoverMutex?.listenOnPopoverChanged(() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } - }); + @override + void dispose() { + widget.popoverMutex?.removePopoverListener(_onPopoverChanged); + focusNode.removeListener(_onFocusChanged); + focusNode.dispose(); + + super.dispose(); } @override @@ -662,15 +662,16 @@ class _FieldNameTextFieldState extends State { ); } - @override - void dispose() { - focusNode.removeListener(() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - }); - focusNode.dispose(); - super.dispose(); + void _onFocusChanged() { + if (focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + } + + void _onPopoverChanged() { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart index da66618d52..fddc97b0f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart @@ -206,22 +206,23 @@ class CreateOptionTextField extends StatefulWidget { } class _CreateOptionTextFieldState extends State { - late final FocusNode _focusNode; + final focusNode = FocusNode(); @override void initState() { super.initState(); - _focusNode = FocusNode() - ..addListener(() { - if (_focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - }); - widget.popoverMutex?.listenOnPopoverChanged(() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - }); + + focusNode.addListener(_onFocusChanged); + widget.popoverMutex?.addPopoverListener(_onPopoverChanged); + } + + @override + void dispose() { + widget.popoverMutex?.removePopoverListener(_onPopoverChanged); + focusNode.removeListener(_onFocusChanged); + focusNode.dispose(); + + super.dispose(); } @override @@ -234,7 +235,7 @@ class _CreateOptionTextFieldState extends State { child: FlowyTextField( autoClearWhenDone: true, text: text, - focusNode: _focusNode, + focusNode: focusNode, onCanceled: () { context .read() @@ -252,15 +253,16 @@ class _CreateOptionTextFieldState extends State { ); } - @override - void dispose() { - _focusNode.removeListener(() { - if (_focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - }); - _focusNode.dispose(); - super.dispose(); + void _onFocusChanged() { + if (focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + } + + void _onPopoverChanged() { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index 4890a79555..152cb06be4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -70,12 +70,12 @@ class _BlockOptionButtonState extends State { Widget build(BuildContext context) { return PopoverActionList( popoverMutex: PopoverMutex(), + actions: popoverActions, direction: context.read().state.layoutDirection == LayoutDirection.rtlLayout ? PopoverDirection.rightWithCenterAligned : PopoverDirection.leftWithCenterAligned, - actions: popoverActions, onPopupBuilder: () { keepEditorFocusNotifier.increase(); widget.blockComponentState.alwaysShowActions = true; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart index c0f5cc8308..99a516b4d3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; const _maxLengthTwelveHour = 8; @@ -64,7 +63,7 @@ class _TimeTextFieldState extends State { } _focusNode.addListener(_focusNodeListener); - widget.popoverMutex?.listenOnPopoverChanged(_popoverListener); + widget.popoverMutex?.addPopoverListener(_popoverListener); } @override diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index 5ce8ee88df..c1e49cbb32 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -17,6 +17,8 @@ class PopoverActionList extends StatefulWidget { this.direction = PopoverDirection.rightWithTopAligned, this.asBarrier = false, this.offset = Offset.zero, + this.animationDuration = const Duration(), + this.slideDistance = 20, this.constraints = const BoxConstraints( minWidth: 120, maxWidth: 460, @@ -35,6 +37,8 @@ class PopoverActionList extends StatefulWidget { final bool asBarrier; final Offset offset; final BoxConstraints constraints; + final Duration animationDuration; + final double slideDistance; @override State> createState() => _PopoverActionListState(); @@ -55,6 +59,8 @@ class _PopoverActionListState final child = widget.buildChild(popoverController); return AppFlowyPopover( asBarrier: widget.asBarrier, + animationDuration: widget.animationDuration, + slideDistance: widget.slideDistance, controller: popoverController, constraints: widget.constraints, direction: widget.direction, diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml index a5744c1cfb..75b15a0f70 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml @@ -1,4 +1,60 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. + include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + - require_trailing_commas + + - prefer_collection_literals + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + + - sized_box_for_whitespace + - use_decorated_box + + - unnecessary_parenthesis + - unnecessary_await_in_return + - unnecessary_raw_strings + + - avoid_unnecessary_containers + - avoid_redundant_argument_values + - avoid_unused_constructor_parameters + + - always_declare_return_types + + - sort_constructors_first + - unawaited_futures + + - prefer_single_quotes + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +errors: + invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml index 61b6c4de17..75b15a0f70 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml @@ -7,8 +7,14 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. + include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` @@ -22,8 +28,33 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + - require_trailing_commas + + - prefer_collection_literals + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + + - sized_box_for_whitespace + - use_decorated_box + + - unnecessary_parenthesis + - unnecessary_await_in_return + - unnecessary_raw_strings + + - avoid_unnecessary_containers + - avoid_redundant_argument_values + - avoid_unused_constructor_parameters + + - always_declare_return_types + + - sort_constructors_first + - unawaited_futures + + - prefer_single_quotes # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options + +errors: + invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977..7c56964006 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 12.0 diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj index 6edd238e7c..2d5f6c6b7c 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,10 +171,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -185,6 +187,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -272,7 +275,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -349,7 +352,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -398,7 +401,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a335..5e31d3d342 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart index fa9d3fe9aa..c3d2b22d5e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flutter/material.dart'; class PopoverMenu extends StatefulWidget { const PopoverMenu({super.key}); @@ -14,43 +14,32 @@ class _PopoverMenuState extends State { @override Widget build(BuildContext context) { return Material( - type: MaterialType.transparency, - child: Container( - width: 200, - height: 200, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(8)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), // changes position of shadow - ), - ], - ), - child: ListView(children: [ + type: MaterialType.transparency, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(8)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), // changes position of shadow + ), + ], + ), + child: ListView( + children: [ Container( margin: const EdgeInsets.all(8), - child: const Text("Popover", - style: TextStyle( - fontSize: 14, - color: Colors.black, - fontStyle: null, - decoration: null)), - ), - Popover( - triggerActions: - PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - mutex: popOverMutex, - offset: const Offset(10, 0), - popupBuilder: (BuildContext context) { - return const PopoverMenu(); - }, - child: TextButton( - onPressed: () {}, - child: const Text("First"), + child: const Text( + 'Popover', + style: TextStyle( + fontSize: 14, + color: Colors.black, + ), ), ), Popover( @@ -63,33 +52,52 @@ class _PopoverMenuState extends State { }, child: TextButton( onPressed: () {}, - child: const Text("Second"), + child: const Text('First'), ), ), - ]), - )); + Popover( + triggerActions: + PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + mutex: popOverMutex, + offset: const Offset(10, 0), + popupBuilder: (BuildContext context) { + return const PopoverMenu(); + }, + child: TextButton( + onPressed: () {}, + child: const Text('Second'), + ), + ), + ], + ), + ), + ); } } class ExampleButton extends StatelessWidget { - final String label; - final Offset? offset; - final PopoverDirection? direction; - const ExampleButton({ super.key, required this.label, - this.direction, + required this.direction, this.offset = Offset.zero, }); + final String label; + final Offset? offset; + final PopoverDirection direction; + @override Widget build(BuildContext context) { return Popover( triggerActions: PopoverTriggerFlags.click, + animationDuration: Durations.medium1, offset: offset, - direction: direction ?? PopoverDirection.rightWithTopAligned, - child: TextButton(child: Text(label), onPressed: () {}), + direction: direction, + child: TextButton( + child: Text(label), + onPressed: () {}, + ), popupBuilder: (BuildContext context) { return const PopoverMenu(); }, diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart index f4e017aa8f..80c4dc6f72 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; -import "./example_button.dart"; + +import './example_button.dart'; void main() { runApp(const MyApp()); @@ -9,21 +10,11 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'AppFlowy Popover Example'), @@ -34,15 +25,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -52,79 +34,82 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: const Row( - children: [ - Column(children: [ - ExampleButton( - label: "Left top", - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithLeftAligned, - ), - Expanded(child: SizedBox.shrink()), - ExampleButton( - label: "Left bottom", - offset: Offset(0, -10), - direction: PopoverDirection.topWithLeftAligned, - ), - ]), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + body: const Padding( + padding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ExampleButton( - label: "Top", + label: 'Left top', + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithLeftAligned, + ), + ExampleButton( + label: 'Left Center', + offset: Offset(0, -10), + direction: PopoverDirection.rightWithCenterAligned, + ), + ExampleButton( + label: 'Left bottom', + offset: Offset(0, -10), + direction: PopoverDirection.topWithLeftAligned, + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ExampleButton( + label: 'Top', offset: Offset(0, 10), direction: PopoverDirection.bottomWithCenterAligned, ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ExampleButton( - label: "Central", - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithCenterAligned, - ), - ], - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ExampleButton( + label: 'Central', + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithCenterAligned, + ), + ], ), ExampleButton( - label: "Bottom", + label: 'Bottom', offset: Offset(0, -10), direction: PopoverDirection.topWithCenterAligned, ), ], ), - ), - Column( - children: [ - ExampleButton( - label: "Right top", - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithRightAligned, - ), - Expanded(child: SizedBox.shrink()), - ExampleButton( - label: "Right bottom", - offset: Offset(0, -10), - direction: PopoverDirection.topWithRightAligned, - ), - ], - ) - ], + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ExampleButton( + label: 'Right top', + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithRightAligned, + ), + ExampleButton( + label: 'Right Center', + offset: Offset(0, 10), + direction: PopoverDirection.leftWithCenterAligned, + ), + ExampleButton( + label: 'Right bottom', + offset: Offset(0, -10), + direction: PopoverDirection.topWithRightAligned, + ), + ], + ), + ], + ), ), ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj index c84862c675..d9ae3f484a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -182,7 +182,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fb7259e177..5b055a3a37 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.17.0 <3.0.0" + sdk: ">=3.3.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart index ff54eaac61..1a61851a71 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart @@ -27,7 +27,9 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower { @override void updateRenderObject( - BuildContext context, PopoverRenderFollowerLayer renderObject) { + BuildContext context, + PopoverRenderFollowerLayer renderObject, + ) { final screenSize = MediaQuery.of(context).size; renderObject ..screenSize = screenSize @@ -40,8 +42,6 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower { } class PopoverRenderFollowerLayer extends RenderFollowerLayer { - Size screenSize; - PopoverRenderFollowerLayer({ required super.link, super.showWhenUnlinked = true, @@ -52,6 +52,8 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer { required this.screenSize, }); + Size screenSize; + @override void paint(PaintingContext context, Offset offset) { super.paint(context, offset); @@ -59,13 +61,6 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer { if (link.leader == null) { return; } - - if (link.leader!.offset.dx + link.leaderSize!.width + size.width > - screenSize.width) { - debugPrint("over flow"); - } - debugPrint( - "right: ${link.leader!.offset.dx + link.leaderSize!.width + size.width}, screen with: ${screenSize.width}"); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart index 85a6e326e2..861d988982 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart @@ -1,14 +1,11 @@ import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; + import './popover.dart'; class PopoverLayoutDelegate extends SingleChildLayoutDelegate { - PopoverLink link; - PopoverDirection direction; - final Offset offset; - final EdgeInsets windowPadding; - PopoverLayoutDelegate({ required this.link, required this.direction, @@ -16,6 +13,11 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { required this.windowPadding, }); + PopoverLink link; + PopoverDirection direction; + final Offset offset; + final EdgeInsets windowPadding; + @override bool shouldRelayout(PopoverLayoutDelegate oldDelegate) { if (direction != oldDelegate.direction) { @@ -52,141 +54,26 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { maxHeight: constraints.maxHeight - windowPadding.top - windowPadding.bottom, ); - // assert(link.leaderSize != null); - // // if (link.leaderSize == null) { - // // return constraints.loosen(); - // // } - // final anchorRect = Rect.fromLTWH( - // link.leaderOffset!.dx, - // link.leaderOffset!.dy, - // link.leaderSize!.width, - // link.leaderSize!.height, - // ); - // BoxConstraints childConstraints; - // switch (direction) { - // case PopoverDirection.topLeft: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // anchorRect.top, - // )); - // break; - // case PopoverDirection.topRight: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.right, - // anchorRect.top, - // )); - // break; - // case PopoverDirection.bottomLeft: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // constraints.maxHeight - anchorRect.bottom, - // )); - // break; - // case PopoverDirection.bottomRight: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.right, - // constraints.maxHeight - anchorRect.bottom, - // )); - // break; - // case PopoverDirection.center: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth, - // constraints.maxHeight, - // )); - // break; - // case PopoverDirection.topWithLeftAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.left, - // anchorRect.top, - // )); - // break; - // case PopoverDirection.topWithCenterAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth, - // anchorRect.top, - // )); - // break; - // case PopoverDirection.topWithRightAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.right, - // anchorRect.top, - // )); - // break; - // case PopoverDirection.rightWithTopAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.right, - // constraints.maxHeight - anchorRect.top, - // )); - // break; - // case PopoverDirection.rightWithCenterAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.right, - // constraints.maxHeight, - // )); - // break; - // case PopoverDirection.rightWithBottomAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth - anchorRect.right, - // anchorRect.bottom, - // )); - // break; - // case PopoverDirection.bottomWithLeftAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // constraints.maxHeight - anchorRect.bottom, - // )); - // break; - // case PopoverDirection.bottomWithCenterAligned: - // childConstraints = BoxConstraints.loose(Size( - // constraints.maxWidth, - // constraints.maxHeight - anchorRect.bottom, - // )); - // break; - // case PopoverDirection.bottomWithRightAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.right, - // constraints.maxHeight - anchorRect.bottom, - // )); - // break; - // case PopoverDirection.leftWithTopAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // constraints.maxHeight - anchorRect.top, - // )); - // break; - // case PopoverDirection.leftWithCenterAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // constraints.maxHeight, - // )); - // break; - // case PopoverDirection.leftWithBottomAligned: - // childConstraints = BoxConstraints.loose(Size( - // anchorRect.left, - // anchorRect.bottom, - // )); - // break; - // case PopoverDirection.custom: - // childConstraints = constraints.loosen(); - // break; - // default: - // throw UnimplementedError(); - // } - // return childConstraints; } @override Offset getPositionForChild(Size size, Size childSize) { - if (link.leaderSize == null) { + final leaderOffset = link.leaderOffset; + final leaderSize = link.leaderSize; + + if (leaderOffset == null || leaderSize == null) { return Offset.zero; } + final anchorRect = Rect.fromLTWH( - link.leaderOffset!.dx + offset.dx, - link.leaderOffset!.dy + offset.dy, - link.leaderSize!.width, - link.leaderSize!.height, + leaderOffset.dx + offset.dx, + leaderOffset.dy + offset.dy, + leaderSize.width, + leaderSize.height, ); + Offset position; + switch (direction) { case PopoverDirection.topLeft: position = Offset( @@ -287,27 +174,35 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { default: throw UnimplementedError(); } + return Offset( math.max( - windowPadding.left, - math.min( - windowPadding.left + size.width - childSize.width, position.dx)), + windowPadding.left, + math.min( + windowPadding.left + size.width - childSize.width, + position.dx, + ), + ), math.max( - windowPadding.top, - math.min( - windowPadding.top + size.height - childSize.height, position.dy)), + windowPadding.top, + math.min( + windowPadding.top + size.height - childSize.height, + position.dy, + ), + ), ); } } class PopoverTarget extends SingleChildRenderObjectWidget { - final PopoverLink link; const PopoverTarget({ super.key, super.child, required this.link, }); + final PopoverLink link; + @override PopoverTargetRenderBox createRenderObject(BuildContext context) { return PopoverTargetRenderBox( @@ -317,14 +212,20 @@ class PopoverTarget extends SingleChildRenderObjectWidget { @override void updateRenderObject( - BuildContext context, PopoverTargetRenderBox renderObject) { + BuildContext context, + PopoverTargetRenderBox renderObject, + ) { renderObject.link = link; } } class PopoverTargetRenderBox extends RenderProxyBox { + PopoverTargetRenderBox({ + required this.link, + RenderBox? child, + }) : super(child); + PopoverLink link; - PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child); @override bool get alwaysNeedsCompositing => true; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart index f68fe95445..bb62b5171a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart @@ -3,11 +3,15 @@ import 'dart:collection'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; -typedef EntryMap = LinkedHashMap; +typedef _EntryMap = LinkedHashMap; class RootOverlayEntry { - final EntryMap _entries = EntryMap(); - RootOverlayEntry(); + final _EntryMap _entries = _EntryMap(); + + bool contains(PopoverState state) => _entries.containsKey(state); + + bool get isEmpty => _entries.isEmpty; + bool get isNotEmpty => _entries.isNotEmpty; void addEntry( BuildContext context, @@ -15,62 +19,54 @@ class RootOverlayEntry { OverlayEntry entry, bool asBarrier, ) { - _entries[newState] = OverlayEntryContext(entry, newState, asBarrier); + _entries[newState] = OverlayEntryContext( + entry, + newState, + asBarrier, + ); Overlay.of(context).insert(entry); } - bool contains(PopoverState oldState) { - return _entries.containsKey(oldState); - } - - void removeEntry(PopoverState oldState) { - if (_entries.isEmpty) return; - - final removedEntry = _entries.remove(oldState); + void removeEntry(PopoverState state) { + final removedEntry = _entries.remove(state); removedEntry?.overlayEntry.remove(); } - bool get isEmpty => _entries.isEmpty; - - bool get isNotEmpty => _entries.isNotEmpty; - - bool hasEntry() { - return _entries.isNotEmpty; - } - PopoverState? popEntry() { - if (_entries.isEmpty) return null; + if (isEmpty) { + return null; + } final lastEntry = _entries.values.last; _entries.remove(lastEntry.popoverState); lastEntry.overlayEntry.remove(); lastEntry.popoverState.widget.onClose?.call(); - if (lastEntry.asBarrier) { - return lastEntry.popoverState; - } else { - return popEntry(); - } + return lastEntry.asBarrier ? lastEntry.popoverState : popEntry(); } } class OverlayEntryContext { - final bool asBarrier; - final PopoverState popoverState; - final OverlayEntry overlayEntry; - OverlayEntryContext( this.overlayEntry, this.popoverState, this.asBarrier, ); + + final OverlayEntry overlayEntry; + final PopoverState popoverState; + final bool asBarrier; } class PopoverMask extends StatelessWidget { - final void Function() onTap; - final Decoration? decoration; + const PopoverMask({ + super.key, + required this.onTap, + this.decoration, + }); - const PopoverMask({super.key, required this.onTap, this.decoration}); + final VoidCallback onTap; + final Decoration? decoration; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart index 8ab88e98e1..be20803eba 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart @@ -5,22 +5,18 @@ import 'popover.dart'; /// If multiple popovers are exclusive, /// pass the same mutex to them. class PopoverMutex { - final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier(); PopoverMutex(); + final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier(); + + void addPopoverListener(VoidCallback listener) { + _stateNotifier.addListener(listener); + } + void removePopoverListener(VoidCallback listener) { _stateNotifier.removeListener(listener); } - VoidCallback listenOnPopoverChanged(VoidCallback callback) { - listenerCallback() { - callback(); - } - - _stateNotifier.addListener(listenerCallback); - return listenerCallback; - } - void close() => _stateNotifier.state?.close(); PopoverState? get state => _stateNotifier.state; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 1aad60f3e5..99063e10b3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -54,6 +54,33 @@ enum PopoverClickHandler { } class Popover extends StatefulWidget { + const Popover({ + super.key, + required this.child, + required this.popupBuilder, + this.controller, + this.offset, + this.triggerActions = 0, + this.direction = PopoverDirection.rightWithTopAligned, + this.mutex, + this.windowPadding, + this.onOpen, + this.onClose, + this.canClose, + this.asBarrier = false, + this.clickHandler = PopoverClickHandler.listener, + this.skipTraversal = false, + this.animationDuration = const Duration(milliseconds: 200), + this.beginOpacity = 0.0, + this.endOpacity = 1.0, + this.beginScaleFactor = 0.95, + this.endScaleFactor = 1.0, + this.slideDistance = 20.0, + this.maskDecoration = const BoxDecoration( + color: Color.fromARGB(0, 244, 67, 54), + ), + }); + final PopoverController? controller; /// The offset from the [child] where the popover will be drawn @@ -93,113 +120,69 @@ class Popover extends StatefulWidget { final bool skipTraversal; + /// Animation time of the popover. + final Duration? animationDuration; + + /// The distance of the popover's slide animation. + final double slideDistance; + + /// The scale factor of the popover's scale animation. + final double beginScaleFactor; + final double endScaleFactor; + + /// The opacity of the popover's fade animation. + final double beginOpacity; + final double endOpacity; + /// The content area of the popover. final Widget child; - const Popover({ - super.key, - required this.child, - required this.popupBuilder, - this.controller, - this.offset, - this.maskDecoration = const BoxDecoration( - color: Color.fromARGB(0, 244, 67, 54), - ), - this.triggerActions = 0, - this.direction = PopoverDirection.rightWithTopAligned, - this.mutex, - this.windowPadding, - this.onOpen, - this.onClose, - this.canClose, - this.asBarrier = false, - this.clickHandler = PopoverClickHandler.listener, - this.skipTraversal = false, - }); - @override State createState() => PopoverState(); } -class PopoverState extends State { - static final RootOverlayEntry _rootEntry = RootOverlayEntry(); +class PopoverState extends State with SingleTickerProviderStateMixin { + static final RootOverlayEntry rootEntry = RootOverlayEntry(); + final PopoverLink popoverLink = PopoverLink(); + late final layoutDelegate = PopoverLayoutDelegate( + direction: widget.direction, + link: popoverLink, + offset: widget.offset ?? Offset.zero, + windowPadding: widget.windowPadding ?? EdgeInsets.zero, + ); + + late AnimationController animationController; + late Animation fadeAnimation; + late Animation scaleAnimation; + late Animation slideAnimation; + + // If the widget is disposed, prevent the animation from being called. + bool isDisposed = false; @override void initState() { super.initState(); + widget.controller?._state = this; - } - - void showOverlay() { - close(); - - if (widget.mutex != null) { - widget.mutex?.state = this; - } - final shouldAddMask = _rootEntry.isEmpty; - final newEntry = OverlayEntry(builder: (context) { - final children = []; - if (shouldAddMask) { - children.add( - PopoverMask( - decoration: widget.maskDecoration, - onTap: () async { - if (!(await widget.canClose?.call() ?? true)) { - return; - } - _removeRootOverlay(); - }, - ), - ); - } - - children.add( - PopoverContainer( - direction: widget.direction, - popoverLink: popoverLink, - offset: widget.offset ?? Offset.zero, - windowPadding: widget.windowPadding ?? EdgeInsets.zero, - popupBuilder: widget.popupBuilder, - onClose: close, - onCloseAll: _removeRootOverlay, - skipTraversal: widget.skipTraversal, - ), - ); - - return CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay, - }, - child: FocusScope(child: Stack(children: children)), - ); - }); - _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); - } - - void close({bool notify = true}) { - if (_rootEntry.contains(this)) { - _rootEntry.removeEntry(this); - if (notify) { - widget.onClose?.call(); - } - } - } - - void _removeRootOverlay() { - _rootEntry.popEntry(); - - if (widget.mutex?.state == this) { - widget.mutex?.removeState(); - } + _buildAnimations(); } @override void deactivate() { close(notify: false); + super.deactivate(); } + @override + void dispose() { + isDisposed = true; + animationController.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return PopoverTarget( @@ -208,6 +191,66 @@ class PopoverState extends State { ); } + @override + void reassemble() { + // clear the overlay + while (rootEntry.isNotEmpty) { + rootEntry.popEntry(); + } + + super.reassemble(); + } + + void showOverlay() { + close(); + + if (widget.mutex != null) { + widget.mutex?.state = this; + } + + final shouldAddMask = rootEntry.isEmpty; + rootEntry.addEntry( + context, + this, + OverlayEntry( + builder: (context) => _buildOverlayContent(shouldAddMask), + ), + widget.asBarrier, + ); + + animationController.forward(); + } + + void close({ + bool notify = true, + bool withAnimation = false, + }) { + if (rootEntry.contains(this)) { + void callback() { + rootEntry.removeEntry(this); + if (notify) { + widget.onClose?.call(); + } + } + + if (isDisposed || !withAnimation) { + callback(); + } else { + animationController.reverse().then((_) => callback()); + } + } + } + + void _removeRootOverlay() { + animationController.reverse().then((_) { + rootEntry.popEntry(); + }); + + if (widget.mutex?.state == this) { + widget.mutex?.removeState(); + } + } + Widget _buildChild(BuildContext context) { if (widget.triggerActions == 0) { return widget.child; @@ -247,36 +290,182 @@ class PopoverState extends State { } void _callHandler(VoidCallback handler) { - if (_rootEntry.contains(this)) { + if (rootEntry.contains(this)) { close(); } else { handler(); } } + + Widget _buildOverlayContent(bool shouldAddMask) { + return CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay, + }, + child: FocusScope( + child: Stack( + children: [ + if (shouldAddMask) _buildMask(), + _buildPopoverContainer(), + ], + ), + ), + ); + } + + Widget _buildMask() { + return PopoverMask( + decoration: widget.maskDecoration, + onTap: () async { + if (await widget.canClose?.call() ?? true) { + _removeRootOverlay(); + } + }, + ); + } + + Widget _buildPopoverContainer() { + return AnimatedBuilder( + animation: animationController, + builder: (context, child) { + return Opacity( + opacity: fadeAnimation.value, + child: Transform.scale( + scale: scaleAnimation.value, + child: Transform.translate( + offset: slideAnimation.value, + child: child, + ), + ), + ); + }, + child: PopoverContainer( + delegate: layoutDelegate, + popupBuilder: widget.popupBuilder, + skipTraversal: widget.skipTraversal, + onClose: close, + onCloseAll: _removeRootOverlay, + ), + ); + } + + void _buildAnimations() { + animationController = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + fadeAnimation = _buildFadeAnimation(); + scaleAnimation = _buildScaleAnimation(); + slideAnimation = _buildSlideAnimation(); + } + + Animation _buildFadeAnimation() { + return Tween( + begin: widget.beginOpacity, + end: widget.endOpacity, + ).animate( + CurvedAnimation( + parent: animationController, + curve: Curves.easeInOut, + ), + ); + } + + Animation _buildScaleAnimation() { + return Tween( + begin: widget.beginScaleFactor, + end: widget.endScaleFactor, + ).animate( + CurvedAnimation( + parent: animationController, + curve: Curves.easeOutCubic, + ), + ); + } + + Animation _buildSlideAnimation() { + final values = _getSlideAnimationValues(); + return Tween( + begin: values.$1, + end: values.$2, + ).animate( + CurvedAnimation( + parent: animationController, + curve: Curves.easeOutCubic, + ), + ); + } + + (Offset, Offset) _getSlideAnimationValues() { + final slideDistance = widget.slideDistance; + + switch (widget.direction) { + case PopoverDirection.bottomWithLeftAligned: + return ( + Offset(-slideDistance, -slideDistance), + Offset.zero, + ); + case PopoverDirection.bottomWithCenterAligned: + return ( + Offset(0, -slideDistance), + Offset.zero, + ); + case PopoverDirection.bottomWithRightAligned: + return ( + Offset(slideDistance, -slideDistance), + Offset.zero, + ); + case PopoverDirection.topWithLeftAligned: + return ( + Offset(-slideDistance, slideDistance), + Offset.zero, + ); + case PopoverDirection.topWithCenterAligned: + return ( + Offset(0, slideDistance), + Offset.zero, + ); + case PopoverDirection.topWithRightAligned: + return ( + Offset(slideDistance, slideDistance), + Offset.zero, + ); + case PopoverDirection.leftWithTopAligned: + case PopoverDirection.leftWithCenterAligned: + case PopoverDirection.leftWithBottomAligned: + return ( + Offset(slideDistance, 0), + Offset.zero, + ); + case PopoverDirection.rightWithTopAligned: + case PopoverDirection.rightWithCenterAligned: + case PopoverDirection.rightWithBottomAligned: + return ( + Offset(-slideDistance, 0), + Offset.zero, + ); + default: + return (Offset.zero, Offset.zero); + } + } } class PopoverContainer extends StatefulWidget { - final Widget? Function(BuildContext context) popupBuilder; - final PopoverDirection direction; - final PopoverLink popoverLink; - final Offset offset; - final EdgeInsets windowPadding; - final void Function() onClose; - final void Function() onCloseAll; - final bool skipTraversal; - const PopoverContainer({ super.key, required this.popupBuilder, - required this.direction, - required this.popoverLink, - required this.offset, - required this.windowPadding, + required this.delegate, required this.onClose, required this.onCloseAll, required this.skipTraversal, }); + final Widget? Function(BuildContext context) popupBuilder; + final void Function() onClose; + final void Function() onCloseAll; + final bool skipTraversal; + final PopoverLayoutDelegate delegate; + @override State createState() => PopoverContainerState(); @@ -302,12 +491,7 @@ class PopoverContainerState extends State { autofocus: true, skipTraversal: widget.skipTraversal, child: CustomSingleChildLayout( - delegate: PopoverLayoutDelegate( - direction: widget.direction, - link: widget.popoverLink, - offset: widget.offset, - windowPadding: widget.windowPadding, - ), + delegate: widget.delegate, child: widget.popupBuilder(context), ), ); diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml index 9dd11b27ac..5d6e335621 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml @@ -4,8 +4,8 @@ version: 0.0.1 homepage: https://appflowy.io environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.1" + flutter: ">=3.22.0" + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: @@ -14,41 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.1 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages + flutter_lints: ^4.0.0 diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 035f41f359..b6007ad069 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -1,37 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; +import 'package:flutter/material.dart'; class AppFlowyPopover extends StatelessWidget { - final Widget child; - final PopoverController? controller; - final Widget Function(BuildContext context) popupBuilder; - final PopoverDirection direction; - final int triggerActions; - final BoxConstraints constraints; - final VoidCallback? onOpen; - final VoidCallback? onClose; - final Future Function()? canClose; - final PopoverMutex? mutex; - final Offset? offset; - final bool asBarrier; - final EdgeInsets margin; - final EdgeInsets windowPadding; - final Color? decorationColor; - final BorderRadius? borderRadius; - - /// The widget that will be used to trigger the popover. - /// - /// Why do we need this? - /// Because if the parent widget of the popover is GestureDetector, - /// the conflict won't be resolve by using Listener, we want these two gestures exclusive. - final PopoverClickHandler clickHandler; - - /// If true the popover will not participate in focus traversal. - /// - final bool skipTraversal; - const AppFlowyPopover({ super.key, required this.child, @@ -52,12 +23,46 @@ class AppFlowyPopover extends StatelessWidget { this.skipTraversal = false, this.decorationColor, this.borderRadius, + this.animationDuration = const Duration(), + this.slideDistance = 20.0, }); + final Widget child; + final PopoverController? controller; + final Widget Function(BuildContext context) popupBuilder; + final PopoverDirection direction; + final int triggerActions; + final BoxConstraints constraints; + final VoidCallback? onOpen; + final VoidCallback? onClose; + final Future Function()? canClose; + final PopoverMutex? mutex; + final Offset? offset; + final bool asBarrier; + final EdgeInsets margin; + final EdgeInsets windowPadding; + final Color? decorationColor; + final BorderRadius? borderRadius; + final Duration animationDuration; + final double slideDistance; + + /// The widget that will be used to trigger the popover. + /// + /// Why do we need this? + /// Because if the parent widget of the popover is GestureDetector, + /// the conflict won't be resolve by using Listener, we want these two gestures exclusive. + final PopoverClickHandler clickHandler; + + /// If true the popover will not participate in focus traversal. + /// + final bool skipTraversal; + @override Widget build(BuildContext context) { return Popover( controller: controller, + animationDuration: animationDuration, + slideDistance: slideDistance, onOpen: onOpen, onClose: onClose, canClose: canClose, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 8e27e7709b..744d039b5e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1535,10 +1535,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: "direct dev" description: @@ -1933,10 +1933,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" string_validator: dependency: "direct main" description: @@ -2238,10 +2238,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: