mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -84,5 +84,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -84,5 +84,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -83,5 +83,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
FieldType.Translate,
|
||||
];
|
||||
|
||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user