feat: AI translation in Database (#5515)

* chore: add tranlate field type

* chore: integrate ai translate

* chore: integrate client api

* chore: implement UI
This commit is contained in:
Nathan.fooo
2024-06-12 16:32:28 +08:00
committed by GitHub
parent 815c99710e
commit 3d7a500550
74 changed files with 1833 additions and 148 deletions

View File

@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
@ -98,6 +99,12 @@ class CardCellBuilder {
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Translate => TranslateCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
_ => throw UnimplementedError,
};
}

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class TranslateCardCellStyle extends CardCellStyle {
const TranslateCardCellStyle({
required super.padding,
required this.textStyle,
});
final TextStyle textStyle;
}
class TranslateCardCell extends CardCell<TranslateCardCellStyle> {
const TranslateCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
final DatabaseController databaseController;
final CellContext cellContext;
@override
State<TranslateCardCell> createState() => _TranslateCellState();
}
class _TranslateCellState extends State<TranslateCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return TranslateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
},
child: BlocBuilder<TranslateCellBloc, TranslateCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -84,5 +84,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -84,5 +84,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -83,5 +83,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -27,50 +27,55 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
onExit: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: GridSize.headerHeight,
),
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 8),
],
),
),
);
},

View File

@ -0,0 +1,99 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => TranslateMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = false,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: GridSize.headerHeight,
),
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<TranslateMouseNotifier>(
builder: (
BuildContext context,
TranslateMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 8),
],
),
),
);
},
);
}
}
class TranslateMouseNotifier extends ChangeNotifier {
TranslateMouseNotifier();
bool _onEnter = false;
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
@ -120,6 +121,12 @@ class EditableCellBuilder {
skin: IEditableSummaryCellSkin.fromStyle(style),
key: key,
),
FieldType.Translate => EditableTranslateCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTranslateCellSkin.fromStyle(style),
key: key,
),
_ => throw UnimplementedError(),
};
}

View File

@ -177,7 +177,7 @@ class SummaryButton extends StatelessWidget {
},
finish: (_) {
return FlowyTooltip(
message: LocaleKeys.tooltip_genSummary.tr(),
message: LocaleKeys.tooltip_aiGenerate.tr(),
child: Container(
width: 26,
height: 26,

View File

@ -0,0 +1,250 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/translate_row_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class IEditableTranslateCellSkin {
const IEditableTranslateCellSkin();
factory IEditableTranslateCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridTranslateCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailTranslateCellSkin(),
EditableCellStyle.mobileGrid => MobileGridTranslateCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailTranslateCellSkin(),
};
}
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
}
class EditableTranslateCell extends EditableCellWidget {
EditableTranslateCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableTranslateCellSkin skin;
@override
GridEditableTextCell<EditableTranslateCell> createState() =>
_TranslateCellState();
}
class _TranslateCellState extends GridEditableTextCell<EditableTranslateCell> {
late final TextEditingController _textEditingController;
late final cellBloc = TranslateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
void dispose() {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<TranslateCellBloc, TranslateCellState>(
listener: (context, state) {
_textEditingController.text = state.content;
},
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void onRequestFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc.add(
TranslateCellEvent.updateCell(_textEditingController.text.trim()),
);
}
return super.focusChanged();
}
}
class TranslateCellAccessory extends StatelessWidget {
const TranslateCellAccessory({
required this.viewId,
required this.rowId,
required this.fieldId,
super.key,
});
final String viewId;
final String rowId;
final String fieldId;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TranslateRowBloc(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
),
child: BlocBuilder<TranslateRowBloc, TranslateRowState>(
builder: (context, state) {
return const Row(
children: [TranslateButton(), HSpace(6), CopyButton()],
);
},
),
);
}
}
class TranslateButton extends StatelessWidget {
const TranslateButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<TranslateRowBloc, TranslateRowState>(
builder: (context, state) {
return state.loadingState.map(
loading: (_) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
},
finish: (_) {
return FlowyTooltip(
message: LocaleKeys.tooltip_aiGenerate.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_summary_generate_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
context
.read<TranslateRowBloc>()
.add(const TranslateRowEvent.startTranslate());
},
),
),
);
},
);
},
);
}
}
class CopyButton extends StatelessWidget {
const CopyButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<TranslateCellBloc, TranslateCellState>(
builder: (blocContext, state) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: state.content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
},
),
),
);
},
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => TranslateMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<TranslateMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<TranslateMouseNotifier>(
builder: (
BuildContext context,
TranslateMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
),
);
},
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TranslateCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -21,6 +21,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Relation,
FieldType.Summary,
FieldType.Translate,
];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {

View File

@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
@ -33,6 +34,7 @@ abstract class TypeOptionEditorFactory {
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(),
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,168 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/translate_type_option_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import './builder.dart';
class TranslateTypeOptionEditorFactory implements TypeOptionEditorFactory {
const TranslateTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = TranslateTypeOptionPB.fromBuffer(field.typeOptionData);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
LocaleKeys.grid_field_translateTo.tr(),
),
const HSpace(6),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: BlocProvider(
create: (context) => TranslateTypeOptionBloc(option: typeOption),
child: BlocConsumer<TranslateTypeOptionBloc,
TranslateTypeOptionState>(
listenWhen: (previous, current) =>
previous.option != current.option,
listener: (context, state) {
onTypeOptionUpdated(state.option.writeToBuffer());
},
builder: (context, state) {
return _wrapLanguageListPopover(
context,
state,
popoverMutex,
SelectLanguageButton(
language: state.language,
),
);
},
),
),
),
],
),
);
}
Widget _wrapLanguageListPopover(
BuildContext blocContext,
TranslateTypeOptionState state,
PopoverMutex popoverMutex,
Widget child,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return LanguageList(
onSelected: (language) {
blocContext
.read<TranslateTypeOptionBloc>()
.add(TranslateTypeOptionEvent.selectLanguage(language));
PopoverContainer.of(popoverContext).close();
},
selectedLanguage: state.option.language,
);
},
child: child,
);
}
}
class SelectLanguageButton extends StatelessWidget {
const SelectLanguageButton({required this.language, super.key});
final String language;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: FlowyButton(text: FlowyText(language)),
);
}
}
class LanguageList extends StatelessWidget {
const LanguageList({
super.key,
required this.onSelected,
required this.selectedLanguage,
});
final Function(TranslateLanguagePB) onSelected;
final TranslateLanguagePB selectedLanguage;
@override
Widget build(BuildContext context) {
final cells = TranslateLanguagePB.values.map((languageType) {
return LanguageCell(
languageType: languageType,
onSelected: onSelected,
isSelected: languageType == selectedLanguage,
);
}).toList();
return SizedBox(
width: 180,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class LanguageCell extends StatelessWidget {
const LanguageCell({
required this.languageType,
required this.onSelected,
required this.isSelected,
super.key,
});
final Function(TranslateLanguagePB) onSelected;
final TranslateLanguagePB languageType;
final bool isSelected;
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(languageTypeToLanguage(languageType)),
rightIcon: checkmark,
onTap: () => onSelected(languageType),
),
);
}
}