mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: replace overlay with popover (#1250)
This commit is contained in:
parent
8d6e1cdaa1
commit
ca8be6ab10
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class FlowyButton extends StatelessWidget {
|
||||
hoverColor: hoverColor,
|
||||
),
|
||||
onHover: onHover,
|
||||
setSelected: () => isSelected,
|
||||
isSelected: () => isSelected,
|
||||
builder: (context, onHover) => _render(),
|
||||
),
|
||||
);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user