chore: replace overlay with popover (#1250)

This commit is contained in:
Nathan.fooo 2022-10-08 17:10:04 +08:00 committed by GitHub
parent 8d6e1cdaa1
commit ca8be6ab10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 393 additions and 504 deletions

View file

@ -68,7 +68,6 @@ class _SettingButtonState extends State<_SettingButton> {
child: FlowyIconButton( child: FlowyIconButton(
hoverColor: theme.hover, hoverColor: theme.hover,
width: 22, width: 22,
onPressed: () {},
icon: Padding( icon: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0), padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
child: svgWidget("grid/setting/setting", color: theme.iconColor), child: svgWidget("grid/setting/setting", color: theme.iconColor),

View file

@ -11,12 +11,11 @@ import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart'; import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@ -130,7 +129,6 @@ class DocumentShareButton extends StatelessWidget {
}, },
child: BlocBuilder<DocShareBloc, DocShareState>( child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) { builder: (context, state) {
final theme = context.watch<AppTheme>();
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSetting>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSetting, Locale>( child: Selector<AppearanceSetting, Locale>(
@ -140,14 +138,7 @@ class DocumentShareButton extends StatelessWidget {
height: 30, height: 30,
width: 100, width: 100,
), ),
child: RoundedTextButton( child: const ShareActionList(),
title: LocaleKeys.shareAction_buttonText.tr(),
fontSize: 12,
borderRadius: Corners.s6Border,
color: theme.main1,
onPressed: () =>
_showActionList(context, const Offset(0, 10)),
),
), ),
), ),
); );
@ -171,11 +162,30 @@ class DocumentShareButton extends StatelessWidget {
} }
void _handleExportError(FlowyError error) {} void _handleExportError(FlowyError error) {}
}
void _showActionList(BuildContext context, Offset offset) { class ShareActionList extends StatelessWidget {
final actionList = ShareActions(onSelected: (result) { const ShareActionList({Key? key}) : super(key: key);
result.fold(() {}, (action) {
switch (action) { @override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return PopoverActionList<ShareActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: ShareAction.values
.map((action) => ShareActionWrapper(action))
.toList(),
withChild: (controller) {
return RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
fontSize: 12,
borderRadius: Corners.s6Border,
color: theme.main1,
onPressed: () => controller.show(),
);
},
onSelected: (action, controller) {
switch (action.inner) {
case ShareAction.markdown: case ShareAction.markdown:
context context
.read<DocShareBloc>() .read<DocShareBloc>()
@ -189,53 +199,18 @@ class DocumentShareButton extends StatelessWidget {
.show(context); .show(context);
break; break;
} }
}); controller.close();
}); },
actionList.show(
context,
anchorDirection: AnchorDirection.bottomWithRightAligned,
anchorOffset: offset,
); );
} }
} }
class ShareActions with ActionList<ShareActionWrapper>, FlowyOverlayDelegate {
final Function(dartz.Option<ShareAction>) onSelected;
final _items =
ShareAction.values.map((action) => ShareActionWrapper(action)).toList();
ShareActions({required this.onSelected});
@override
double get itemHeight => 22;
@override
List<ShareActionWrapper> get items => _items;
@override
void Function(dartz.Option<ShareActionWrapper> p1) get selectCallback =>
(result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(
dartz.some(wrapper.inner),
),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() => onSelected(dartz.none());
}
enum ShareAction { enum ShareAction {
markdown, markdown,
copyLink, copyLink,
} }
class ShareActionWrapper extends ActionItem { class ShareActionWrapper extends ActionCell {
final ShareAction inner; final ShareAction inner;
ShareActionWrapper(this.inner); ShareActionWrapper(this.inner);

View file

@ -1,22 +1,21 @@
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart'; import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:expandable/expandable.dart'; import 'package:expandable/expandable.dart';
import 'package:flowy_infra/icon_data.dart'; import 'package:flowy_infra/icon_data.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart'; import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:dartz/dartz.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import '../menu_app.dart'; import '../menu_app.dart';
import 'add_button.dart'; import 'add_button.dart';
import 'right_click_action.dart';
class MenuAppHeader extends StatelessWidget { class MenuAppHeader extends StatelessWidget {
final AppPB app; final AppPB app;
@ -79,30 +78,23 @@ class MenuAppHeader extends StatelessWidget {
expandableController.toggle(); expandableController.toggle();
} }
}, },
child: GestureDetector( child: AppActionList(onSelected: (action) {
behavior: HitTestBehavior.opaque, switch (action) {
onTap: () => ExpandableController.of(context, case AppDisclosureAction.rename:
rebuildOnChange: false, required: true) NavigatorTextFieldDialog(
?.toggle(), title: LocaleKeys.menuAppHeader_renameDialog.tr(),
onSecondaryTap: () { value: context.read<AppBloc>().state.app.name,
final actionList = AppDisclosureActionSheet( confirm: (newValue) {
onSelected: (action) => _handleAction(context, action), context.read<AppBloc>().add(AppEvent.rename(newValue));
); },
actionList.show( ).show(context);
context,
anchorDirection: AnchorDirection.bottomWithCenterAligned, break;
); case AppDisclosureAction.delete:
}, context.read<AppBloc>().add(const AppEvent.delete());
child: BlocSelector<AppBloc, AppState, AppPB>( break;
selector: (state) => state.app, }
builder: (context, app) => FlowyText.medium( }),
app.name,
fontSize: 12,
color: theme.textColor,
overflow: TextOverflow.ellipsis,
),
),
),
), ),
); );
} }
@ -123,26 +115,6 @@ class MenuAppHeader extends StatelessWidget {
).padding(right: MenuAppSizes.headerPadding), ).padding(right: MenuAppSizes.headerPadding),
); );
} }
void _handleAction(BuildContext context, Option<AppDisclosureAction> action) {
action.fold(() {}, (action) {
switch (action) {
case AppDisclosureAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
value: context.read<AppBloc>().state.app.name,
confirm: (newValue) {
context.read<AppBloc>().add(AppEvent.rename(newValue));
},
).show(context);
break;
case AppDisclosureAction.delete:
context.read<AppBloc>().add(const AppEvent.delete());
break;
}
});
}
} }
enum AppDisclosureAction { enum AppDisclosureAction {
@ -169,3 +141,57 @@ extension AppDisclosureExtension on AppDisclosureAction {
} }
} }
} }
class AppActionList extends StatelessWidget {
final Function(AppDisclosureAction) onSelected;
const AppActionList({
required this.onSelected,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
return PopoverActionList<DisclosureActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: AppDisclosureAction.values
.map((action) => DisclosureActionWrapper(action))
.toList(),
withChild: (controller) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => ExpandableController.of(context,
rebuildOnChange: false, required: true)
?.toggle(),
onSecondaryTap: () {
controller.show();
},
child: BlocSelector<AppBloc, AppState, AppPB>(
selector: (state) => state.app,
builder: (context, app) => FlowyText.medium(
app.name,
fontSize: 12,
color: theme.textColor,
overflow: TextOverflow.ellipsis,
),
),
);
},
onSelected: (action, controller) {
onSelected(action.inner);
controller.close();
},
);
}
}
class DisclosureActionWrapper extends ActionCell {
final AppDisclosureAction inner;
DisclosureActionWrapper(this.inner);
@override
Widget? icon(Color iconColor) => inner.icon(iconColor);
@override
String get name => inner.name;
}

View file

@ -1,51 +0,0 @@
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'header.dart';
class AppDisclosureActionSheet
with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
final Function(dartz.Option<AppDisclosureAction>) onSelected;
final _items = AppDisclosureAction.values
.map((action) => DisclosureActionWrapper(action))
.toList();
AppDisclosureActionSheet({
required this.onSelected,
});
@override
List<DisclosureActionWrapper> get items => _items;
@override
void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback =>
(result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(
dartz.some(wrapper.inner),
),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() {
onSelected(dartz.none());
}
}
class DisclosureActionWrapper extends ActionItem {
final AppDisclosureAction inner;
DisclosureActionWrapper(this.inner);
@override
Widget? icon(Color iconColor) => inner.icon(iconColor);
@override
String get name => inner.name;
}

View file

@ -1,130 +0,0 @@
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra/theme.dart';
import 'package:provider/provider.dart';
import 'item.dart';
// [[Widget: LifeCycle]]
// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
class ViewDisclosureButton extends StatelessWidget
with ActionList<ViewDisclosureActionWrapper>, FlowyOverlayDelegate {
final Function() onTap;
final Function(dartz.Option<ViewDisclosureAction>) onSelected;
final _items = ViewDisclosureAction.values
.map((action) => ViewDisclosureActionWrapper(action))
.toList();
ViewDisclosureButton({
Key? key,
required this.onTap,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyIconButton(
iconPadding: const EdgeInsets.all(5),
width: 26,
onPressed: () {
onTap();
show(context);
},
icon: svgWidget("editor/details", color: theme.iconColor),
);
}
@override
List<ViewDisclosureActionWrapper> get items => _items;
@override
void Function(dartz.Option<ViewDisclosureActionWrapper> p1)
get selectCallback => (result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(dartz.some(wrapper.inner)),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() {
onSelected(dartz.none());
}
}
class ViewDisclosureRegion extends StatelessWidget
with ActionList<ViewDisclosureActionWrapper>, FlowyOverlayDelegate {
final Widget child;
final Function() onTap;
final Function(dartz.Option<ViewDisclosureAction>) onSelected;
final _items = ViewDisclosureAction.values
.map((action) => ViewDisclosureActionWrapper(action))
.toList();
ViewDisclosureRegion(
{Key? key,
required this.onSelected,
required this.onTap,
required this.child})
: super(key: key);
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (event) => _handleClick(event, context),
child: child,
);
}
@override
FlowyOverlayDelegate? get delegate => this;
@override
List<ViewDisclosureActionWrapper> get items => _items;
@override
void Function(dartz.Option<ViewDisclosureActionWrapper> p1)
get selectCallback => (result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(dartz.some(wrapper.inner)),
);
};
@override
void didRemove() {
onSelected(dartz.none());
}
void _handleClick(PointerDownEvent event, BuildContext context) {
if (event.kind == PointerDeviceKind.mouse &&
event.buttons == kSecondaryMouseButton) {
RenderBox box = context.findRenderObject() as RenderBox;
Offset position = box.localToGlobal(Offset.zero);
double x = event.position.dx - position.dx - box.size.width;
double y = event.position.dy - position.dy - box.size.height;
onTap();
show(context, anchorOffset: Offset(x, y));
}
}
}
class ViewDisclosureActionWrapper extends ActionItem {
final ViewDisclosureAction inner;
ViewDisclosureActionWrapper(this.inner);
@override
Widget? icon(Color iconColor) => inner.icon(iconColor);
@override
String get name => inner.name;
}

View file

@ -3,7 +3,6 @@ import 'package:app_flowy/workspace/application/view/view_bloc.dart';
import 'package:app_flowy/workspace/application/view/view_ext.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart';
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart'; import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart'; import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -16,7 +15,9 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'disclosure_action.dart'; import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class ViewSectionItem extends StatelessWidget { class ViewSectionItem extends StatelessWidget {
@ -37,40 +38,41 @@ class ViewSectionItem extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (ctx) => create: (ctx) => getIt<ViewBloc>(param1: view)
getIt<ViewBloc>(param1: view)..add(const ViewEvent.initial())), ..add(
const ViewEvent.initial(),
)),
], ],
child: BlocBuilder<ViewBloc, ViewState>( child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) { builder: (blocContext, state) {
return ViewDisclosureRegion( return Padding(
onTap: () => context padding: const EdgeInsets.symmetric(vertical: 2),
.read<ViewBloc>() child: InkWell(
.add(const ViewEvent.setIsEditing(true)), onTap: () => onSelected(blocContext.read<ViewBloc>().state.view),
onSelected: (action) { child: FlowyHover(
context style: HoverStyle(hoverColor: theme.bg3),
.read<ViewBloc>() buildWhen: () => !state.isEditing,
.add(const ViewEvent.setIsEditing(false)); builder: (_, onHover) => _render(
_handleAction(context, action); blocContext,
}, onHover,
child: Padding( state,
padding: const EdgeInsets.symmetric(vertical: 2), theme.iconColor,
child: InkWell(
onTap: () => onSelected(context.read<ViewBloc>().state.view),
child: FlowyHover(
style: HoverStyle(hoverColor: theme.bg3),
builder: (_, onHover) =>
_render(context, onHover, state, theme.iconColor),
setSelected: () => state.isEditing || isSelected,
),
), ),
)); isSelected: () => state.isEditing || isSelected,
),
),
);
}, },
), ),
); );
} }
Widget _render( Widget _render(
BuildContext context, bool onHover, ViewState state, Color iconColor) { BuildContext blocContext,
bool onHover,
ViewState state,
Color iconColor,
) {
List<Widget> children = [ List<Widget> children = [
SizedBox( SizedBox(
width: 16, width: 16,
@ -90,11 +92,29 @@ class ViewSectionItem extends StatelessWidget {
if (onHover || state.isEditing) { if (onHover || state.isEditing) {
children.add( children.add(
ViewDisclosureButton( ViewDisclosureButton(
onTap: () => onEdit: (isEdit) =>
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true)), blocContext.read<ViewBloc>().add(ViewEvent.setIsEditing(isEdit)),
onSelected: (action) { onAction: (action) {
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(false)); switch (action) {
_handleAction(context, action); case ViewDisclosureAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.disclosureAction_rename.tr(),
value: blocContext.read<ViewBloc>().state.view.name,
confirm: (newValue) {
blocContext
.read<ViewBloc>()
.add(ViewEvent.rename(newValue));
},
).show(blocContext);
break;
case ViewDisclosureAction.delete:
blocContext.read<ViewBloc>().add(const ViewEvent.delete());
break;
case ViewDisclosureAction.duplicate:
blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
}
}, },
), ),
); );
@ -108,30 +128,6 @@ class ViewSectionItem extends StatelessWidget {
), ),
); );
} }
void _handleAction(
BuildContext context, dartz.Option<ViewDisclosureAction> action) {
action.foldRight({}, (action, previous) {
switch (action) {
case ViewDisclosureAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.disclosureAction_rename.tr(),
value: context.read<ViewBloc>().state.view.name,
confirm: (newValue) {
context.read<ViewBloc>().add(ViewEvent.rename(newValue));
},
).show(context);
break;
case ViewDisclosureAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case ViewDisclosureAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
}
});
}
} }
enum ViewDisclosureAction { enum ViewDisclosureAction {
@ -163,3 +159,51 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
} }
} }
} }
class ViewDisclosureButton extends StatelessWidget {
final Function(bool) onEdit;
final Function(ViewDisclosureAction) onAction;
const ViewDisclosureButton({
required this.onEdit,
required this.onAction,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return PopoverActionList<ViewDisclosureActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: ViewDisclosureAction.values
.map((action) => ViewDisclosureActionWrapper(action))
.toList(),
withChild: (controller) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(5),
width: 26,
icon: svgWidget("editor/details", color: theme.iconColor),
onPressed: () {
onEdit(true);
controller.show();
},
);
},
onSelected: (action, controller) {
onEdit(false);
onAction(action.inner);
controller.close();
},
);
}
}
class ViewDisclosureActionWrapper extends ActionCell {
final ViewDisclosureAction inner;
ViewDisclosureActionWrapper(this.inner);
@override
Widget? icon(Color iconColor) => inner.icon(iconColor);
@override
String get name => inner.name;
}

View file

@ -1,16 +1,15 @@
import 'package:app_flowy/startup/tasks/rust_sdk.dart'; import 'package:app_flowy/startup/tasks/rust_sdk.dart';
import 'package:app_flowy/workspace/presentation/home/toast.dart'; import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -22,41 +21,59 @@ class QuestionBubble extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); return const SizedBox(
return SizedBox(
width: 30, width: 30,
height: 30, height: 30,
child: FlowyTextButton( child: BubbleActionList(),
'?', );
tooltip: LocaleKeys.questionBubble_help.tr(), }
fontSize: 12, }
fontWeight: FontWeight.w600,
fillColor: theme.selector, class BubbleActionList extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, const BubbleActionList({Key? key}) : super(key: key);
radius: BorderRadius.circular(10),
onPressed: () { @override
final actionList = QuestionBubbleActionSheet(onSelected: (result) { Widget build(BuildContext context) {
result.fold(() {}, (action) { final theme = context.watch<AppTheme>();
switch (action) {
case BubbleAction.whatsNews: final List<PopoverAction> actions = [];
_launchURL("https://www.appflowy.io/whatsnew"); actions.addAll(
break; BubbleAction.values.map((action) => BubbleActionWrapper(action)),
case BubbleAction.help: );
_launchURL("https://discord.gg/9Q2xaN37tV"); actions.add(FlowyVersionDescription());
break;
case BubbleAction.debug: return PopoverActionList<PopoverAction>(
_DebugToast().show(); direction: PopoverDirection.topWithRightAligned,
break; actions: actions,
} withChild: (controller) {
}); return FlowyTextButton(
}); '?',
actionList.show( tooltip: LocaleKeys.questionBubble_help.tr(),
context, fontSize: 12,
anchorDirection: AnchorDirection.topWithRightAligned, fontWeight: FontWeight.w600,
anchorOffset: const Offset(0, -10), fillColor: theme.selector,
); mainAxisAlignment: MainAxisAlignment.center,
}, radius: BorderRadius.circular(10),
), onPressed: () => controller.show(),
);
},
onSelected: (action, controller) {
if (action is BubbleActionWrapper) {
switch (action.inner) {
case BubbleAction.whatsNews:
_launchURL("https://www.appflowy.io/whatsnew");
break;
case BubbleAction.help:
_launchURL("https://discord.gg/9Q2xaN37tV");
break;
case BubbleAction.debug:
_DebugToast().show();
break;
}
}
controller.close();
},
); );
} }
@ -101,54 +118,9 @@ class _DebugToast {
} }
} }
class QuestionBubbleActionSheet class FlowyVersionDescription extends CustomActionCell {
with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
final Function(dartz.Option<BubbleAction>) onSelected;
final _items =
BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
QuestionBubbleActionSheet({
required this.onSelected,
});
@override @override
double get itemHeight => 22; Widget buildWithContext(BuildContext context) {
@override
List<BubbleActionWrapper> get items => _items;
@override
void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback =>
(result) {
result.fold(
() => onSelected(dartz.none()),
(wrapper) => onSelected(
dartz.some(wrapper.inner),
),
);
};
@override
FlowyOverlayDelegate? get delegate => this;
@override
void didRemove() {
onSelected(dartz.none());
}
@override
ListOverlayFooter? get footer => ListOverlayFooter(
widget: const FlowyVersionDescription(),
height: 40,
padding: const EdgeInsets.only(top: 6),
);
}
class FlowyVersionDescription extends StatelessWidget {
const FlowyVersionDescription({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FutureBuilder( return FutureBuilder(
@ -165,23 +137,26 @@ class FlowyVersionDescription extends StatelessWidget {
String version = packageInfo.version; String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber; String buildNumber = packageInfo.buildNumber;
return Column( return SizedBox(
mainAxisAlignment: MainAxisAlignment.start, height: 30,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.start,
Divider(height: 1, color: theme.shader6, thickness: 1.0), crossAxisAlignment: CrossAxisAlignment.start,
const VSpace(6), children: [
FlowyText( Divider(height: 1, color: theme.shader6, thickness: 1.0),
"$appName $version.$buildNumber", const VSpace(6),
fontSize: 12, FlowyText(
color: theme.shader4, "$appName $version.$buildNumber",
), fontSize: 12,
], color: theme.shader4,
).padding( ),
horizontal: ActionListSizes.itemHPadding + ActionListSizes.hPadding, ],
).padding(
horizontal: ActionListSizes.itemHPadding,
),
); );
} else { } else {
return const CircularProgressIndicator(); return const SizedBox(height: 30);
} }
}, },
); );
@ -190,7 +165,7 @@ class FlowyVersionDescription extends StatelessWidget {
enum BubbleAction { whatsNews, help, debug } enum BubbleAction { whatsNews, help, debug }
class BubbleActionWrapper extends ActionItem { class BubbleActionWrapper extends ActionCell {
final BubbleAction inner; final BubbleAction inner;
BubbleActionWrapper(this.inner); BubbleActionWrapper(this.inner);

View file

@ -1,3 +1,4 @@
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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/hover.dart';
@ -6,66 +7,90 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:dartz/dartz.dart' as dartz;
abstract class ActionList<T extends ActionItem> { class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
List<T> get items; final List<T> actions;
final Function(T, PopoverController) onSelected;
final BoxConstraints constraints;
final PopoverDirection direction;
final Widget Function(PopoverController) withChild;
String get identifier => toString(); const PopoverActionList({
required this.actions,
required this.withChild,
required this.onSelected,
this.direction = PopoverDirection.rightWithTopAligned,
this.constraints = const BoxConstraints(
minWidth: 120,
maxWidth: 360,
maxHeight: 300,
),
Key? key,
}) : super(key: key);
double get maxWidth => 300; @override
State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();
}
double get minWidth => 120; class _PopoverActionListState<T extends PopoverAction>
extends State<PopoverActionList<T>> {
late PopoverController popoverController;
double get itemHeight => ActionListSizes.itemHeight; @override
void initState() {
popoverController = PopoverController();
super.initState();
}
ListOverlayFooter? get footer => null; @override
Widget build(BuildContext context) {
final child = widget.withChild(popoverController);
void Function(dartz.Option<T>) get selectCallback; return AppFlowyPopover(
controller: popoverController,
constraints: widget.constraints,
direction: widget.direction,
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext popoverContext) {
final List<Widget> children = widget.actions.map((action) {
if (action is ActionCell) {
return ActionCellWidget<T>(
action: action,
itemHeight: ActionListSizes.itemHeight,
onSelected: (action) {
widget.onSelected(action, popoverController);
},
);
} else {
final custom = action as CustomActionCell;
return custom.buildWithContext(context);
}
}).toList();
FlowyOverlayDelegate? get delegate; return IntrinsicHeight(
child: IntrinsicWidth(
void show( child: Column(
BuildContext buildContext, { children: children,
BuildContext? anchorContext, ),
AnchorDirection anchorDirection = AnchorDirection.bottomRight, ),
Offset? anchorOffset,
}) {
ListOverlay.showWithAnchor(
buildContext,
identifier: identifier,
itemCount: items.length,
itemBuilder: (context, index) {
final action = items[index];
return ActionCell<T>(
action: action,
itemHeight: itemHeight,
onSelected: (action) {
FlowyOverlay.of(buildContext).remove(identifier);
selectCallback(dartz.some(action));
},
); );
}, },
anchorContext: anchorContext ?? buildContext, child: child,
anchorDirection: anchorDirection,
constraints: BoxConstraints(
minHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
maxHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
maxWidth: maxWidth,
minWidth: minWidth,
),
delegate: delegate,
anchorOffset: anchorOffset,
footer: footer,
); );
} }
} }
abstract class ActionItem { abstract class ActionCell extends PopoverAction {
Widget? icon(Color iconColor); Widget? icon(Color iconColor);
String get name; String get name;
} }
abstract class CustomActionCell extends PopoverAction {
Widget buildWithContext(BuildContext context);
}
abstract class PopoverAction {}
class ActionListSizes { class ActionListSizes {
static double itemHPadding = 10; static double itemHPadding = 10;
static double itemHeight = 20; static double itemHeight = 20;
@ -73,11 +98,11 @@ class ActionListSizes {
static double hPadding = 10; static double hPadding = 10;
} }
class ActionCell<T extends ActionItem> extends StatelessWidget { class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
final T action; final T action;
final Function(T) onSelected; final Function(T) onSelected;
final double itemHeight; final double itemHeight;
const ActionCell({ const ActionCellWidget({
Key? key, Key? key,
required this.action, required this.action,
required this.onSelected, required this.onSelected,
@ -86,8 +111,9 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final actionCell = action as ActionCell;
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final icon = action.icon(theme.iconColor); final icon = actionCell.icon(theme.iconColor);
return FlowyHover( return FlowyHover(
style: HoverStyle(hoverColor: theme.hover), style: HoverStyle(hoverColor: theme.hover),
@ -99,7 +125,13 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)], if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
FlowyText.medium(action.name, fontSize: 12), Expanded(
child: FlowyText.medium(
actionCell.name,
fontSize: 12,
overflow: TextOverflow.visible,
),
),
], ],
), ),
).padding( ).padding(

View file

@ -11,7 +11,7 @@ class AppFlowyPopover extends StatelessWidget {
final Widget Function(BuildContext context) popupBuilder; final Widget Function(BuildContext context) popupBuilder;
final PopoverDirection direction; final PopoverDirection direction;
final int triggerActions; final int triggerActions;
final BoxConstraints? constraints; final BoxConstraints constraints;
final void Function()? onClose; final void Function()? onClose;
final PopoverMutex? mutex; final PopoverMutex? mutex;
final Offset? offset; final Offset? offset;
@ -58,12 +58,12 @@ class AppFlowyPopover extends StatelessWidget {
class _PopoverContainer extends StatelessWidget { class _PopoverContainer extends StatelessWidget {
final Widget child; final Widget child;
final BoxConstraints? constraints; final BoxConstraints constraints;
final EdgeInsets margin; final EdgeInsets margin;
const _PopoverContainer({ const _PopoverContainer({
required this.child, required this.child,
required this.margin, required this.margin,
this.constraints, required this.constraints,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -74,6 +74,7 @@ class _PopoverContainer extends StatelessWidget {
theme.surface, theme.surface,
theme.shadowColor.withOpacity(0.15), theme.shadowColor.withOpacity(0.15),
); );
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Container( child: Container(
@ -81,6 +82,14 @@ class _PopoverContainer extends StatelessWidget {
decoration: decoration, decoration: decoration,
constraints: constraints, constraints: constraints,
child: child, child: child,
// SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: ConstrainedBox(
// constraints: constraints,
// child: child,
// ),
// ),
), ),
); );
} }

View file

@ -37,7 +37,7 @@ class FlowyButton extends StatelessWidget {
hoverColor: hoverColor, hoverColor: hoverColor,
), ),
onHover: onHover, onHover: onHover,
setSelected: () => isSelected, isSelected: () => isSelected,
builder: (context, onHover) => _render(), builder: (context, onHover) => _render(),
), ),
); );

View file

@ -8,19 +8,21 @@ class FlowyHover extends StatefulWidget {
final HoverStyle style; final HoverStyle style;
final HoverBuilder? builder; final HoverBuilder? builder;
final Widget? child; final Widget? child;
final bool Function()? setSelected; final bool Function()? isSelected;
final void Function(bool)? onHover; final void Function(bool)? onHover;
final MouseCursor? cursor; final MouseCursor? cursor;
final bool Function()? buildWhen;
const FlowyHover( const FlowyHover({
{Key? key, Key? key,
this.builder, this.builder,
this.child, this.child,
required this.style, required this.style,
this.setSelected, this.isSelected,
this.onHover, this.onHover,
this.cursor}) this.cursor,
: super(key: key); this.buildWhen,
}) : super(key: key);
@override @override
State<FlowyHover> createState() => _FlowyHoverState(); State<FlowyHover> createState() => _FlowyHoverState();
@ -35,15 +37,23 @@ class _FlowyHoverState extends State<FlowyHover> {
cursor: widget.cursor != null ? widget.cursor! : SystemMouseCursors.click, cursor: widget.cursor != null ? widget.cursor! : SystemMouseCursors.click,
opaque: false, opaque: false,
onEnter: (p) { onEnter: (p) {
setState(() => _onHover = true); if (_onHover) return;
if (widget.onHover != null) {
widget.onHover!(true); if (widget.buildWhen?.call() ?? true) {
setState(() => _onHover = true);
if (widget.onHover != null) {
widget.onHover!(true);
}
} }
}, },
onExit: (p) { onExit: (p) {
setState(() => _onHover = false); if (_onHover == false) return;
if (widget.onHover != null) {
widget.onHover!(false); if (widget.buildWhen?.call() ?? true) {
setState(() => _onHover = false);
if (widget.onHover != null) {
widget.onHover!(false);
}
} }
}, },
child: renderWidget(), child: renderWidget(),
@ -52,8 +62,8 @@ class _FlowyHoverState extends State<FlowyHover> {
Widget renderWidget() { Widget renderWidget() {
var showHover = _onHover; var showHover = _onHover;
if (!showHover && widget.setSelected != null) { if (!showHover && widget.isSelected != null) {
showHover = widget.setSelected!(); showHover = widget.isSelected!();
} }
final child = widget.child ?? widget.builder!(context, _onHover); final child = widget.child ?? widget.builder!(context, _onHover);