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(
hoverColor: theme.hover,
width: 22,
onPressed: () {},
icon: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
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/dialogs.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:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.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_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@ -130,7 +129,6 @@ class DocumentShareButton extends StatelessWidget {
},
child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) {
final theme = context.watch<AppTheme>();
return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSetting, Locale>(
@ -140,14 +138,7 @@ class DocumentShareButton extends StatelessWidget {
height: 30,
width: 100,
),
child: RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
fontSize: 12,
borderRadius: Corners.s6Border,
color: theme.main1,
onPressed: () =>
_showActionList(context, const Offset(0, 10)),
),
child: const ShareActionList(),
),
),
);
@ -171,11 +162,30 @@ class DocumentShareButton extends StatelessWidget {
}
void _handleExportError(FlowyError error) {}
}
void _showActionList(BuildContext context, Offset offset) {
final actionList = ShareActions(onSelected: (result) {
result.fold(() {}, (action) {
switch (action) {
class ShareActionList extends StatelessWidget {
const ShareActionList({Key? key}) : super(key: key);
@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:
context
.read<DocShareBloc>()
@ -189,53 +199,18 @@ class DocumentShareButton extends StatelessWidget {
.show(context);
break;
}
});
});
actionList.show(
context,
anchorDirection: AnchorDirection.bottomWithRightAligned,
anchorOffset: offset,
controller.close();
},
);
}
}
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 {
markdown,
copyLink,
}
class ShareActionWrapper extends ActionItem {
class ShareActionWrapper extends ActionCell {
final ShareAction 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/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:expandable/expandable.dart';
import 'package:flowy_infra/icon_data.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_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:dartz/dartz.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flowy_infra/image.dart';
import '../menu_app.dart';
import 'add_button.dart';
import 'right_click_action.dart';
class MenuAppHeader extends StatelessWidget {
final AppPB app;
@ -79,30 +78,23 @@ class MenuAppHeader extends StatelessWidget {
expandableController.toggle();
}
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => ExpandableController.of(context,
rebuildOnChange: false, required: true)
?.toggle(),
onSecondaryTap: () {
final actionList = AppDisclosureActionSheet(
onSelected: (action) => _handleAction(context, action),
);
actionList.show(
context,
anchorDirection: AnchorDirection.bottomWithCenterAligned,
);
},
child: BlocSelector<AppBloc, AppState, AppPB>(
selector: (state) => state.app,
builder: (context, app) => FlowyText.medium(
app.name,
fontSize: 12,
color: theme.textColor,
overflow: TextOverflow.ellipsis,
),
),
),
child: AppActionList(onSelected: (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;
}
}),
),
);
}
@ -123,26 +115,6 @@ class MenuAppHeader extends StatelessWidget {
).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 {
@ -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/presentation/home/menu/menu.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:flowy_infra/theme.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: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
class ViewSectionItem extends StatelessWidget {
@ -37,40 +38,41 @@ class ViewSectionItem extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (ctx) =>
getIt<ViewBloc>(param1: view)..add(const ViewEvent.initial())),
create: (ctx) => getIt<ViewBloc>(param1: view)
..add(
const ViewEvent.initial(),
)),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return ViewDisclosureRegion(
onTap: () => context
.read<ViewBloc>()
.add(const ViewEvent.setIsEditing(true)),
onSelected: (action) {
context
.read<ViewBloc>()
.add(const ViewEvent.setIsEditing(false));
_handleAction(context, action);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
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,
),
builder: (blocContext, state) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: InkWell(
onTap: () => onSelected(blocContext.read<ViewBloc>().state.view),
child: FlowyHover(
style: HoverStyle(hoverColor: theme.bg3),
buildWhen: () => !state.isEditing,
builder: (_, onHover) => _render(
blocContext,
onHover,
state,
theme.iconColor,
),
));
isSelected: () => state.isEditing || isSelected,
),
),
);
},
),
);
}
Widget _render(
BuildContext context, bool onHover, ViewState state, Color iconColor) {
BuildContext blocContext,
bool onHover,
ViewState state,
Color iconColor,
) {
List<Widget> children = [
SizedBox(
width: 16,
@ -90,11 +92,29 @@ class ViewSectionItem extends StatelessWidget {
if (onHover || state.isEditing) {
children.add(
ViewDisclosureButton(
onTap: () =>
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true)),
onSelected: (action) {
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(false));
_handleAction(context, action);
onEdit: (isEdit) =>
blocContext.read<ViewBloc>().add(ViewEvent.setIsEditing(isEdit)),
onAction: (action) {
switch (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 {
@ -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/workspace/presentation/home/toast.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: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/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:styled_widget/styled_widget.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@ -22,41 +21,59 @@ class QuestionBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return SizedBox(
return const SizedBox(
width: 30,
height: 30,
child: FlowyTextButton(
'?',
tooltip: LocaleKeys.questionBubble_help.tr(),
fontSize: 12,
fontWeight: FontWeight.w600,
fillColor: theme.selector,
mainAxisAlignment: MainAxisAlignment.center,
radius: BorderRadius.circular(10),
onPressed: () {
final actionList = QuestionBubbleActionSheet(onSelected: (result) {
result.fold(() {}, (action) {
switch (action) {
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;
}
});
});
actionList.show(
context,
anchorDirection: AnchorDirection.topWithRightAligned,
anchorOffset: const Offset(0, -10),
);
},
),
child: BubbleActionList(),
);
}
}
class BubbleActionList extends StatelessWidget {
const BubbleActionList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final List<PopoverAction> actions = [];
actions.addAll(
BubbleAction.values.map((action) => BubbleActionWrapper(action)),
);
actions.add(FlowyVersionDescription());
return PopoverActionList<PopoverAction>(
direction: PopoverDirection.topWithRightAligned,
actions: actions,
withChild: (controller) {
return FlowyTextButton(
'?',
tooltip: LocaleKeys.questionBubble_help.tr(),
fontSize: 12,
fontWeight: FontWeight.w600,
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
with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
final Function(dartz.Option<BubbleAction>) onSelected;
final _items =
BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
QuestionBubbleActionSheet({
required this.onSelected,
});
class FlowyVersionDescription extends CustomActionCell {
@override
double get itemHeight => 22;
@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) {
Widget buildWithContext(BuildContext context) {
final theme = context.watch<AppTheme>();
return FutureBuilder(
@ -165,23 +137,26 @@ class FlowyVersionDescription extends StatelessWidget {
String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(height: 1, color: theme.shader6, thickness: 1.0),
const VSpace(6),
FlowyText(
"$appName $version.$buildNumber",
fontSize: 12,
color: theme.shader4,
),
],
).padding(
horizontal: ActionListSizes.itemHPadding + ActionListSizes.hPadding,
return SizedBox(
height: 30,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(height: 1, color: theme.shader6, thickness: 1.0),
const VSpace(6),
FlowyText(
"$appName $version.$buildNumber",
fontSize: 12,
color: theme.shader4,
),
],
).padding(
horizontal: ActionListSizes.itemHPadding,
),
);
} else {
return const CircularProgressIndicator();
return const SizedBox(height: 30);
}
},
);
@ -190,7 +165,7 @@ class FlowyVersionDescription extends StatelessWidget {
enum BubbleAction { whatsNews, help, debug }
class BubbleActionWrapper extends ActionItem {
class BubbleActionWrapper extends ActionCell {
final BubbleAction 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_ui/flowy_infra_ui.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:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:dartz/dartz.dart' as dartz;
abstract class ActionList<T extends ActionItem> {
List<T> get items;
class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
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;
void show(
BuildContext buildContext, {
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));
},
return IntrinsicHeight(
child: IntrinsicWidth(
child: Column(
children: children,
),
),
);
},
anchorContext: anchorContext ?? buildContext,
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,
child: child,
);
}
}
abstract class ActionItem {
abstract class ActionCell extends PopoverAction {
Widget? icon(Color iconColor);
String get name;
}
abstract class CustomActionCell extends PopoverAction {
Widget buildWithContext(BuildContext context);
}
abstract class PopoverAction {}
class ActionListSizes {
static double itemHPadding = 10;
static double itemHeight = 20;
@ -73,11 +98,11 @@ class ActionListSizes {
static double hPadding = 10;
}
class ActionCell<T extends ActionItem> extends StatelessWidget {
class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
final T action;
final Function(T) onSelected;
final double itemHeight;
const ActionCell({
const ActionCellWidget({
Key? key,
required this.action,
required this.onSelected,
@ -86,8 +111,9 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final actionCell = action as ActionCell;
final theme = context.watch<AppTheme>();
final icon = action.icon(theme.iconColor);
final icon = actionCell.icon(theme.iconColor);
return FlowyHover(
style: HoverStyle(hoverColor: theme.hover),
@ -99,7 +125,13 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
child: Row(
children: [
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(

View File

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

View File

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

View File

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