diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 5a2d069c36..6f75b60ade 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -200,7 +200,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 + fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 @@ -227,4 +227,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.15.2 +COCOAPODS: 1.11.3 diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart index cc6c9b43aa..d683a9b72d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -19,7 +20,10 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: const BoxConstraints(minWidth: double.infinity), + constraints: BoxConstraints( + minWidth: double.infinity, + minHeight: GridSize.headerHeight, + ), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( shape: WidgetStateProperty.all( diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart new file mode 100644 index 0000000000..f31a4a1c91 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'translate_cell_bloc.freezed.dart'; + +class TranslateCellBloc extends Bloc { + TranslateCellBloc({ + required this.cellController, + }) : super(TranslateCellState.initial(cellController)) { + _dispatch(); + _startListening(); + } + + final TranslateCellController cellController; + void Function()? _onCellChangedFn; + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener( + onCellChanged: _onCellChangedFn!, + onFieldChanged: _onFieldChangedListener, + ); + } + await cellController.dispose(); + return super.close(); + } + + void _dispatch() { + on( + (event, emit) async { + await event.when( + didReceiveCellUpdate: (cellData) { + emit( + state.copyWith(content: cellData ?? ""), + ); + }, + didUpdateField: (fieldInfo) { + final wrap = fieldInfo.wrapCellContent; + if (wrap != null) { + emit(state.copyWith(wrap: wrap)); + } + }, + updateCell: (text) async { + if (state.content != text) { + emit(state.copyWith(content: text)); + await cellController.saveCellData(text); + + // If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string. + // So for every cell data that will be formatted in the backend. + // It needs to get the formatted data after saving. + add( + TranslateCellEvent.didReceiveCellUpdate( + cellController.getCellData() ?? "", + ), + ); + } + }, + ); + }, + ); + } + + void _startListening() { + _onCellChangedFn = cellController.addListener( + onCellChanged: (cellContent) { + if (!isClosed) { + add( + TranslateCellEvent.didReceiveCellUpdate(cellContent ?? ""), + ); + } + }, + onFieldChanged: _onFieldChangedListener, + ); + } + + void _onFieldChangedListener(FieldInfo fieldInfo) { + if (!isClosed) { + add(TranslateCellEvent.didUpdateField(fieldInfo)); + } + } +} + +@freezed +class TranslateCellEvent with _$TranslateCellEvent { + const factory TranslateCellEvent.didReceiveCellUpdate(String? cellContent) = + _DidReceiveCellUpdate; + const factory TranslateCellEvent.didUpdateField(FieldInfo fieldInfo) = + _DidUpdateField; + const factory TranslateCellEvent.updateCell(String text) = _UpdateCell; +} + +@freezed +class TranslateCellState with _$TranslateCellState { + const factory TranslateCellState({ + required String content, + required bool wrap, + }) = _TranslateCellState; + + factory TranslateCellState.initial(TranslateCellController cellController) { + final wrap = cellController.fieldInfo.wrapCellContent; + return TranslateCellState( + content: cellController.getCellData() ?? "", + wrap: wrap ?? true, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart new file mode 100644 index 0000000000..4778df2c2a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart @@ -0,0 +1,100 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'translate_row_bloc.freezed.dart'; + +class TranslateRowBloc extends Bloc { + TranslateRowBloc({ + required this.viewId, + required this.rowId, + required this.fieldId, + }) : super(TranslateRowState.initial()) { + _dispatch(); + } + + final String viewId; + final String rowId; + final String fieldId; + + void _dispatch() { + on( + (event, emit) async { + event.when( + startTranslate: () { + final params = TranslateRowPB( + viewId: viewId, + rowId: rowId, + fieldId: fieldId, + ); + emit( + state.copyWith( + loadingState: const LoadingState.loading(), + error: null, + ), + ); + + DatabaseEventTranslateRow(params).send().then( + (result) => { + if (!isClosed) + add(TranslateRowEvent.finishTranslate(result)), + }, + ); + }, + finishTranslate: (result) { + result.fold( + (s) => { + emit( + state.copyWith( + loadingState: const LoadingState.finish(), + error: null, + ), + ), + }, + (err) => { + emit( + state.copyWith( + loadingState: const LoadingState.finish(), + error: err, + ), + ), + }, + ); + }, + ); + }, + ); + } +} + +@freezed +class TranslateRowEvent with _$TranslateRowEvent { + const factory TranslateRowEvent.startTranslate() = _DidStartTranslate; + const factory TranslateRowEvent.finishTranslate( + FlowyResult result, + ) = _DidFinishTranslate; +} + +@freezed +class TranslateRowState with _$TranslateRowState { + const factory TranslateRowState({ + required LoadingState loadingState, + required FlowyError? error, + }) = _TranslateRowState; + + factory TranslateRowState.initial() { + return const TranslateRowState( + loadingState: LoadingState.finish(), + error: null, + ); + } +} + +@freezed +class LoadingState with _$LoadingState { + const factory LoadingState.loading() = _Loading; + const factory LoadingState.finish() = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart index e4866a1517..e9457e23dc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart @@ -16,6 +16,7 @@ typedef TimestampCellController = CellController; typedef URLCellController = CellController; typedef RelationCellController = CellController; typedef SummaryCellController = CellController; +typedef TranslateCellController = CellController; CellController makeCellController( DatabaseController databaseController, @@ -145,6 +146,18 @@ CellController makeCellController( ), cellDataPersistence: TextCellDataPersistence(), ); + case FieldType.Translate: + return TranslateCellController( + viewId: viewId, + fieldController: fieldController, + cellContext: cellContext, + rowCache: rowCache, + cellDataLoader: CellDataLoader( + parser: StringCellDataParser(), + reloadOnFieldChange: true, + ), + cellDataPersistence: TextCellDataPersistence(), + ); } throw UnimplementedError; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart new file mode 100644 index 0000000000..8f9bd8559c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart @@ -0,0 +1,72 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/translate_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; + +part 'translate_type_option_bloc.freezed.dart'; + +class TranslateTypeOptionBloc + extends Bloc { + TranslateTypeOptionBloc({required TranslateTypeOptionPB option}) + : super(TranslateTypeOptionState.initial(option)) { + on( + (event, emit) async { + event.when( + selectLanguage: (languageType) { + emit( + state.copyWith( + option: _updateLanguage(languageType), + language: languageTypeToLanguage(languageType), + ), + ); + }, + ); + }, + ); + } + + TranslateTypeOptionPB _updateLanguage(TranslateLanguagePB languageType) { + state.option.freeze(); + return state.option.rebuild((option) { + option.language = languageType; + }); + } +} + +@freezed +class TranslateTypeOptionEvent with _$TranslateTypeOptionEvent { + const factory TranslateTypeOptionEvent.selectLanguage( + TranslateLanguagePB languageType, + ) = _SelectLanguage; +} + +@freezed +class TranslateTypeOptionState with _$TranslateTypeOptionState { + const factory TranslateTypeOptionState({ + required TranslateTypeOptionPB option, + required String language, + }) = _TranslateTypeOptionState; + + factory TranslateTypeOptionState.initial(TranslateTypeOptionPB option) => + TranslateTypeOptionState( + option: option, + language: languageTypeToLanguage(option.language), + ); +} + +String languageTypeToLanguage(TranslateLanguagePB langaugeType) { + switch (langaugeType) { + case TranslateLanguagePB.Chinese: + return 'Chinese'; + case TranslateLanguagePB.English: + return 'English'; + case TranslateLanguagePB.French: + return 'French'; + case TranslateLanguagePB.German: + return 'German'; + default: + Log.error('Unknown language type: $langaugeType'); + return 'English'; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart index f93a7a3d02..f318bad9d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart @@ -4,8 +4,6 @@ abstract class TypeOptionParser { T fromBuffer(List buffer); } - - class NumberTypeOptionDataParser extends TypeOptionParser { @override NumberTypeOptionPB fromBuffer(List buffer) { @@ -51,3 +49,11 @@ class RelationTypeOptionDataParser return RelationTypeOptionPB.fromBuffer(buffer); } } + +class TranslateTypeOptionDataParser + extends TypeOptionParser { + @override + TranslateTypeOptionPB fromBuffer(List buffer) { + return TranslateTypeOptionPB.fromBuffer(buffer); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart index 5e96e35f1f..c028df7ced 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart @@ -14,6 +14,7 @@ class GridSize { static double get cellVPadding => 10 * scale; static double get popoverItemHeight => 26 * scale; static double get typeOptionSeparatorHeight => 4 * scale; + static double get newPropertyButtonWidth => 140 * scale; static EdgeInsets get cellContentInsets => EdgeInsets.symmetric( horizontal: GridSize.cellHPadding, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index 1a63e3c678..fd09b6a591 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -93,9 +93,12 @@ class _GridFieldCellState extends State { onFieldInserted: widget.onFieldInsertedOnEitherSide, ); }, - child: FieldCellButton( - field: widget.fieldInfo.field, - onTap: widget.onTap, + child: SizedBox( + height: 40, + child: FieldCellButton( + field: widget.fieldInfo.field, + onTap: widget.onTap, + ), ), ); @@ -217,6 +220,12 @@ class FieldCellButton extends StatelessWidget { field.fieldType.svgData, color: Theme.of(context).iconTheme.color, ), + rightIcon: field.fieldType.rightIcon != null + ? FlowySvg( + field.fieldType.rightIcon!, + blendMode: null, + ) + : null, radius: radius, text: FlowyText.medium( field.name, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index 79ad37c706..3b6b4320d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -151,7 +151,10 @@ class _CellTrailing extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: GridSize.trailHeaderPadding, + constraints: BoxConstraints( + maxWidth: GridSize.newPropertyButtonWidth, + minHeight: GridSize.headerHeight, + ), margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m), decoration: BoxDecoration( border: Border( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart index 35c4127dfd..90bfbbca13 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart @@ -196,7 +196,10 @@ class _CreateFieldButtonState extends State { @override Widget build(BuildContext context) { return Container( - width: 200, + constraints: BoxConstraints( + maxWidth: GridSize.newPropertyButtonWidth, + minHeight: GridSize.headerHeight, + ), decoration: _getDecoration(context), child: FlowyButton( margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart index a002f73f88..2b83b590d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart @@ -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, }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart new file mode 100644 index 0000000000..e9c233af18 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart @@ -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 { + const TranslateCardCell({ + super.key, + required super.style, + required this.databaseController, + required this.cellContext, + }); + + final DatabaseController databaseController; + final CellContext cellContext; + + @override + State createState() => _TranslateCellState(); +} + +class _TranslateCellState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return TranslateCellBloc( + cellController: makeCellController( + widget.databaseController, + widget.cellContext, + ).as(), + ); + }, + child: BlocBuilder( + 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), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart index 431ac44029..785c1f917a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart @@ -84,5 +84,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), + FieldType.Translate: SummaryCardCellStyle( + padding: padding, + textStyle: textStyle, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index ebe1537cbb..333886c6f9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -84,5 +84,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), + FieldType.Translate: SummaryCardCellStyle( + padding: padding, + textStyle: textStyle, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart index df162abcba..71a4d54b95 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart @@ -83,5 +83,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), + FieldType.Translate: SummaryCardCellStyle( + padding: padding, + textStyle: textStyle, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart index 4ef64ced2f..09d7d43b14 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart @@ -27,50 +27,55 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { onExit: (p) => Provider.of(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( - 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( + 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), + ], + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart new file mode 100644 index 0000000000..b96c4320a7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart @@ -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(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(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( + 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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart new file mode 100644 index 0000000000..a12159b83d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart @@ -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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index 66beb7c437..2ac68ed034 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -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(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart index ccd42bc96c..4b291719bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart @@ -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, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart new file mode 100644 index 0000000000..2d3fd33751 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart @@ -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 createState() => + _TranslateCellState(); +} + +class _TranslateCellState extends GridEditableTextCell { + 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( + 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 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( + 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( + 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() + .add(const TranslateRowEvent.startTranslate()); + }, + ), + ), + ); + }, + ); + }, + ); + } +} + +class CopyButton extends StatelessWidget { + const CopyButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + 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()); + }, + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart new file mode 100644 index 0000000000..5b17bef39a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart @@ -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(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(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( + 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), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart new file mode 100644 index 0000000000..a9b141fe9a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart @@ -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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index 02bc0700a4..afcb7da38f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -21,6 +21,7 @@ const List _supportedFieldTypes = [ FieldType.CreatedTime, FieldType.Relation, FieldType.Summary, + FieldType.Translate, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart index 2db2c09544..8ec91bdbaf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart @@ -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(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart new file mode 100644 index 0000000000..2c6815f621 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart @@ -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( + 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() + .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), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index 9d26149a21..99812b7c7f 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -22,6 +22,7 @@ extension FieldTypeExtension on FieldType { FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(), FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(), FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(), + FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(), _ => throw UnimplementedError(), }; @@ -38,9 +39,16 @@ extension FieldTypeExtension on FieldType { FieldType.CreatedTime => FlowySvgs.created_at_s, FieldType.Relation => FlowySvgs.relation_s, FieldType.Summary => FlowySvgs.ai_summary_s, + FieldType.Translate => FlowySvgs.ai_translate_s, _ => throw UnimplementedError(), }; + FlowySvgData? get rightIcon => switch (this) { + FieldType.Summary => FlowySvgs.ai_indicator_s, + FieldType.Translate => FlowySvgs.ai_indicator_s, + _ => null, + }; + Color get mobileIconBackgroundColor => switch (this) { FieldType.RichText => const Color(0xFFBECCFF), FieldType.Number => const Color(0xFFCABDFF), @@ -54,6 +62,7 @@ extension FieldTypeExtension on FieldType { FieldType.Checklist => const Color(0xFF98F4CD), FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Summary => const Color(0xFFBECCFF), + FieldType.Translate => const Color(0xFFBECCFF), _ => throw UnimplementedError(), }; @@ -71,6 +80,7 @@ extension FieldTypeExtension on FieldType { FieldType.Checklist => const Color(0xFF42AD93), FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Summary => const Color(0xFF6859A7), + FieldType.Translate => const Color(0xFF6859A7), _ => throw UnimplementedError(), }; } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 46320ba2b9..56acca07bc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bytes", @@ -772,7 +772,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "again", "anyhow", @@ -819,7 +819,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "futures-channel", "futures-util", @@ -1059,7 +1059,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -1084,7 +1084,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "async-trait", @@ -1441,7 +1441,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -1956,6 +1956,7 @@ name = "flowy-database-pub" version = "0.1.0" dependencies = [ "anyhow", + "client-api", "collab", "collab-entity", "lib-infra", @@ -2853,7 +2854,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "futures-util", @@ -2870,7 +2871,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -3302,7 +3303,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "reqwest", @@ -5792,7 +5793,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index cac7634e62..a3f6e2f4e1 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" } [dependencies] serde_json.workspace = true diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 31ad62bb38..5f9068878d 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -236,7 +236,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bytes", @@ -562,7 +562,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "again", "anyhow", @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "futures-channel", "futures-util", @@ -787,7 +787,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "async-trait", @@ -981,7 +981,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1026,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -1341,6 +1341,7 @@ name = "flowy-database-pub" version = "0.1.0" dependencies = [ "anyhow", + "client-api", "collab", "collab-entity", "lib-infra", @@ -1881,7 +1882,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "futures-util", @@ -1898,7 +1899,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -2199,7 +2200,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "reqwest", @@ -2916,7 +2917,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -2936,7 +2937,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3004,19 +3004,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -3901,7 +3888,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 4029f36c85..a7b70b144f 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -55,7 +55,7 @@ yrs = "0.18.8" # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 4c77b63af4..3253cd85fa 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bytes", @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "again", "anyhow", @@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "futures-channel", "futures-util", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "async-trait", @@ -1317,7 +1317,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.10", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1428,7 +1428,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -1993,6 +1993,7 @@ name = "flowy-database-pub" version = "0.1.0" dependencies = [ "anyhow", + "client-api", "collab", "collab-entity", "lib-infra", @@ -2927,7 +2928,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "futures-util", @@ -2944,7 +2945,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -3381,7 +3382,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "reqwest", @@ -4888,7 +4889,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4909,7 +4910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.55", @@ -5887,7 +5888,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index be1e6b69f5..721c2dccee 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/flowy_icons/16x/ai_indicator.svg b/frontend/resources/flowy_icons/16x/ai_indicator.svg new file mode 100644 index 0000000000..690c01ac0b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_indicator.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/ai_keyword.svg b/frontend/resources/flowy_icons/16x/ai_keyword.svg new file mode 100644 index 0000000000..60e87b4913 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_keyword.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_tag.svg b/frontend/resources/flowy_icons/16x/ai_tag.svg new file mode 100644 index 0000000000..317ad8fb2f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_translate.svg b/frontend/resources/flowy_icons/16x/ai_translate.svg new file mode 100644 index 0000000000..7e9706c4af --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_translate.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8bd65ffc1f..0c716a5e44 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -241,7 +241,7 @@ "viewDataBase": "View database", "referencePage": "This {name} is referenced", "addBlockBelow": "Add a block below", - "genSummary": "Generate summary" + "aiGenerate": "Generate" }, "sideBar": { "closeSidebar": "Close sidebar", @@ -873,6 +873,8 @@ "checklistFieldName": "Checklist", "relationFieldName": "Relation", "summaryFieldName": "AI Summary", + "translateFieldName": "AI Translate", + "translateTo": "Translate to", "numberFormat": "Number format", "dateFormat": "Date format", "includeTime": "Include time", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index c79dab339a..26c616c416 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bytes", @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "again", "anyhow", @@ -711,7 +711,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "futures-channel", "futures-util", @@ -920,7 +920,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "bincode", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "async-trait", @@ -1165,7 +1165,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1265,7 +1265,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -1776,6 +1776,7 @@ name = "flowy-database-pub" version = "0.1.0" dependencies = [ "anyhow", + "client-api", "collab", "collab-entity", "lib-infra", @@ -2523,7 +2524,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "futures-util", @@ -2540,7 +2541,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", @@ -2905,7 +2906,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "reqwest", @@ -3781,7 +3782,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3801,7 +3802,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3869,19 +3869,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.47", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -4085,7 +4072,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4106,7 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", @@ -5003,7 +4990,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6282fab3c6..c1363948f0 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -94,7 +94,7 @@ yrs = "0.18.8" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" } [profile.dev] opt-level = 1 diff --git a/frontend/rust-lib/event-integration-test/src/database_event.rs b/frontend/rust-lib/event-integration-test/src/database_event.rs index 1e264e03f8..0209e77561 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -215,6 +215,14 @@ impl EventIntegrationTest { .await; } + pub async fn translate_row(&self, data: TranslateRowPB) { + EventBuilder::new(self.clone()) + .event(DatabaseEvent::TranslateRow) + .payload(data) + .async_send() + .await; + } + pub async fn create_row( &self, view_id: &str, diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs index 36f850dd92..788864be1e 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs @@ -1,2 +1,3 @@ -// mod summarize_row; +// mod summarize_row_test; +// mod translate_row_test; mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row_test.rs similarity index 100% rename from frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row.rs rename to frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row_test.rs diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/translate_row_test.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/translate_row_test.rs new file mode 100644 index 0000000000..8de361b8b3 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/translate_row_test.rs @@ -0,0 +1,54 @@ +use crate::database::af_cloud::util::make_test_summary_grid; +use std::time::Duration; +use tokio::time::sleep; + +use event_integration_test::user_event::user_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_database2::entities::{FieldType, TranslateRowPB}; + +#[tokio::test] +async fn af_cloud_translate_row_test() { + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.af_cloud_sign_up().await; + + // create document and then insert content + let current_workspace = test.get_current_workspace().await; + let initial_data = make_test_summary_grid().to_json_bytes().unwrap(); + let view = test + .create_grid( + ¤t_workspace.id, + "translate database".to_string(), + initial_data, + ) + .await; + + let database_pb = test.get_database(&view.id).await; + let field = test + .get_all_database_fields(&view.id) + .await + .items + .into_iter() + .find(|field| field.field_type == FieldType::Translate) + .unwrap(); + + let row_id = database_pb.rows[0].id.clone(); + let data = TranslateRowPB { + view_id: view.id.clone(), + row_id: row_id.clone(), + field_id: field.id.clone(), + }; + test.translate_row(data).await; + + sleep(Duration::from_secs(1)).await; + let cell = test + .get_text_cell(&view.id, &row_id, &field.id) + .await + .to_lowercase(); + println!("cell: {}", cell); + // default translation is in French. So it should be something like this: + // Prix:2,6 $,Nom du produit:Pomme,Statut:TERMINÉ + assert!(cell.contains("pomme")); + assert!(cell.contains("produit")); + assert!(cell.contains("prix")); +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs index 3bfd07cab4..25ef9920c8 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs @@ -6,6 +6,7 @@ use collab_database::fields::Field; use collab_database::rows::Row; use flowy_database2::entities::FieldType; use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption; +use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption; use flowy_database2::services::field::{ FieldBuilder, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, @@ -61,6 +62,7 @@ fn create_fields() -> Vec { FieldType::Number => fields.push(create_number_field("Price", NumberFormat::USD)), FieldType::SingleSelect => fields.push(create_single_select_field("Status")), FieldType::Summary => fields.push(create_summary_field("AI summary")), + FieldType::Translate => fields.push(create_translate_field("AI Translate")), _ => {}, } } @@ -124,3 +126,14 @@ fn create_summary_field(name: &str) -> Field { .name(name) .build() } + +#[allow(dead_code)] +fn create_translate_field(name: &str) -> Field { + let type_option = TranslateTypeOption { + auto_fill: false, + language_type: 2, + }; + FieldBuilder::new(FieldType::Translate, type_option) + .name(name) + .build() +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 98f08761e6..adbdda6a64 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -22,6 +22,7 @@ use flowy_chat_pub::cloud::{ }; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use flowy_document::deps::DocumentData; use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; @@ -293,6 +294,23 @@ impl DatabaseCloudService for ServerProvider { .await }) } + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let language = language.to_string(); + FutureResult::new(async move { + server? + .database_service() + .translate_database_row(&workspace_id, translate_row, &language) + .await + }) + } } impl DocumentCloudService for ServerProvider { diff --git a/frontend/rust-lib/flowy-database-pub/Cargo.toml b/frontend/rust-lib/flowy-database-pub/Cargo.toml index eb6358cb8c..fb258183a8 100644 --- a/frontend/rust-lib/flowy-database-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-database-pub/Cargo.toml @@ -9,4 +9,5 @@ edition = "2021" lib-infra = { workspace = true } collab-entity = { workspace = true } collab = { workspace = true } -anyhow.workspace = true \ No newline at end of file +anyhow.workspace = true +client-api = { workspace = true } diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 5df6325362..3a43eb36da 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -1,4 +1,5 @@ use anyhow::Error; +pub use client_api::entity::ai_dto::{TranslateItem, TranslateRowResponse}; use collab::core::collab::DataSource; use collab_entity::CollabType; use lib_infra::future::FutureResult; @@ -6,6 +7,7 @@ use std::collections::HashMap; pub type CollabDocStateByOid = HashMap; pub type SummaryRowContent = HashMap; +pub type TranslateRowContent = Vec; /// A trait for database cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. @@ -39,6 +41,13 @@ pub trait DatabaseCloudService: Send + Sync { object_id: &str, summary_row: SummaryRowContent, ) -> FutureResult; + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult; } pub struct DatabaseSnapshot { diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index ad8635d80d..956f58c259 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -449,6 +449,7 @@ pub enum FieldType { CreatedTime = 9, Relation = 10, Summary = 11, + Translate = 12, } impl Display for FieldType { @@ -489,6 +490,7 @@ impl FieldType { FieldType::CreatedTime => "Created time", FieldType::Relation => "Relation", FieldType::Summary => "Summarize", + FieldType::Translate => "Translate", }; s.to_string() } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index 7bcd2292bf..d4f3bedb7c 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB { .cloned::() .unwrap() .try_into(), + FieldType::Translate => condition_and_content + .cloned::() + .unwrap() + .try_into(), }; Self { @@ -156,6 +160,9 @@ impl TryFrom for FilterInner { FieldType::Summary => { BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) }, + FieldType::Translate => { + BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, }; Ok(Self::Data { diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index 14c0613442..35c594a07e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -16,6 +16,7 @@ macro_rules! impl_into_field_type { 9 => FieldType::CreatedTime, 10 => FieldType::Relation, 11 => FieldType::Summary, + 12 => FieldType::Translate, _ => { tracing::error!("🔴Can't parse FieldType from value: {}", ty); FieldType::RichText diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 1071a2630e..5c31d11b0d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -380,3 +380,18 @@ pub struct SummaryRowPB { #[pb(index = 3)] pub field_id: String, } + +#[derive(Debug, Default, Clone, ProtoBuf, Validate)] +pub struct TranslateRowPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub view_id: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub row_id: String, + + #[pb(index = 3)] + #[validate(custom = "required_not_empty_str")] + pub field_id: String, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs index deeb260f0e..ceeeab3874 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs @@ -7,6 +7,7 @@ mod select_option_entities; mod summary_entities; mod text_entities; mod timestamp_entities; +mod translate_entities; mod url_entities; pub use checkbox_entities::*; @@ -18,4 +19,5 @@ pub use select_option_entities::*; pub use summary_entities::*; pub use text_entities::*; pub use timestamp_entities::*; +pub use translate_entities::*; pub use url_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs new file mode 100644 index 0000000000..bb373bb942 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs @@ -0,0 +1,50 @@ +use crate::services::field::translate_type_option::translate::TranslateTypeOption; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct TranslateTypeOptionPB { + #[pb(index = 1)] + pub auto_fill: bool, + + #[pb(index = 2)] + pub language: TranslateLanguagePB, +} + +impl From for TranslateTypeOptionPB { + fn from(value: TranslateTypeOption) -> Self { + TranslateTypeOptionPB { + auto_fill: value.auto_fill, + language: value.language_type.into(), + } + } +} + +impl From for TranslateTypeOption { + fn from(value: TranslateTypeOptionPB) -> Self { + TranslateTypeOption { + auto_fill: value.auto_fill, + language_type: value.language as i64, + } + } +} +#[derive(Clone, Debug, Copy, ProtoBuf_Enum, Default)] +#[repr(i64)] +pub enum TranslateLanguagePB { + Chinese = 0, + #[default] + English = 1, + French = 2, + German = 3, +} + +impl From for TranslateLanguagePB { + fn from(data: i64) -> Self { + match data { + 0 => TranslateLanguagePB::Chinese, + 1 => TranslateLanguagePB::English, + 2 => TranslateLanguagePB::French, + 3 => TranslateLanguagePB::German, + _ => TranslateLanguagePB::English, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 742b0d2fd9..40419119f0 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1104,3 +1104,16 @@ pub(crate) async fn summarize_row_handler( .await?; Ok(()) } + +pub(crate) async fn translate_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let data = data.try_into_inner()?; + let row_id = RowId::from(data.row_id); + manager + .translate_row(data.view_id, row_id, data.field_id) + .await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 7a46332013..02c64da785 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -91,6 +91,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler) // AI .event(DatabaseEvent::SummarizeRow, summarize_row_handler) + .event(DatabaseEvent::TranslateRow, translate_row_handler) } /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) @@ -373,4 +374,7 @@ pub enum DatabaseEvent { #[event(input = "SummaryRowPB")] SummarizeRow = 174, + + #[event(input = "TranslateRowPB")] + TranslateRow = 175, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 283ad989c1..83dcd17431 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -17,15 +17,19 @@ use tracing::{event, instrument, trace}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig}; -use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent}; +use flowy_database_pub::cloud::{ + DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent, +}; use flowy_error::{internal_error, FlowyError, FlowyResult}; use lib_infra::box_any::BoxAny; use lib_infra::priority_task::TaskDispatcher; -use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB}; +use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, FieldType}; use crate::services::cell::stringify_cell; use crate::services::database::DatabaseEditor; use crate::services::database_view::DatabaseLayoutDepsResolver; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; + use crate::services::field_settings::default_field_settings_by_layout_map; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; @@ -459,6 +463,77 @@ impl DatabaseManager { Ok(()) } + #[instrument(level = "debug", skip_all)] + pub async fn translate_row( + &self, + view_id: String, + row_id: RowId, + field_id: String, + ) -> FlowyResult<()> { + let database = self.get_database_with_view_id(&view_id).await?; + let mut translate_row_content = TranslateRowContent::new(); + let mut language = "english".to_string(); + + if let Some(row) = database.get_row(&view_id, &row_id) { + let fields = database.get_fields(&view_id, None); + for field in fields { + // When translate a row, skip the content in the "AI Translate" cell; it does not need to + // be translated. + if field.id != field_id { + if let Some(cell) = row.cells.get(&field.id) { + translate_row_content.push(TranslateItem { + title: field.name.clone(), + content: stringify_cell(cell, &field), + }) + } + } else { + language = TranslateTypeOption::language_from_type( + field + .type_options + .get(&FieldType::Translate.to_string()) + .cloned() + .map(TranslateTypeOption::from) + .unwrap_or_default() + .language_type, + ) + .to_string(); + } + } + } + + // Call the cloud service to summarize the row. + trace!( + "[AI]:translate to {}, content:{:?}", + language, + translate_row_content + ); + let response = self + .cloud_service + .translate_database_row(&self.user.workspace_id()?, translate_row_content, &language) + .await?; + + // Format the response items into a single string + let content = response + .items + .into_iter() + .map(|value| { + value + .into_iter() + .map(|(_k, v)| v.to_string()) + .collect::>() + .join(", ") + }) + .collect::>() + .join(","); + + trace!("[AI]:translate row response: {}", content); + // Update the cell with the response from the cloud service. + database + .update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(content)) + .await?; + Ok(()) + } + /// Only expose this method for testing #[cfg(debug_assertions)] pub fn get_cloud_service(&self) -> &Arc { diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 7212c2fa54..068eea5da4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -262,6 +262,9 @@ impl<'a> CellBuilder<'a> { FieldType::Summary => { cells.insert(field_id, insert_text_cell(cell_str, field)); }, + FieldType::Translate => { + cells.insert(field_id, insert_text_cell(cell_str, field)); + }, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 01056dfeea..8e6549e315 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1703,8 +1703,9 @@ pub async fn update_field_type_option_fn( update.update_type_options(|type_options_update| { event!( tracing::Level::TRACE, - "insert type option to field type: {:?}", - field_type + "insert type option to field type: {:?}, {:?}", + field_type, + type_option_data ); type_options_update.insert(&field_type.to_string(), type_option_data); }); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs index 44d7329567..49451f3820 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs @@ -7,6 +7,7 @@ pub mod selection_type_option; pub mod summary_type_option; pub mod text_type_option; pub mod timestamp_type_option; +pub mod translate_type_option; mod type_option; mod type_option_cell; mod url_type_option; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index e8c3e8b9d8..1627b04465 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -85,6 +85,7 @@ impl CellDataDecoder for RichTextTypeOption { | FieldType::CreatedTime | FieldType::Relation => None, FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))), + FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs new file mode 100644 index 0000000000..08c163748f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs @@ -0,0 +1,2 @@ +pub mod translate; +pub mod translate_entities; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs new file mode 100644 index 0000000000..080411796c --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs @@ -0,0 +1,137 @@ +use crate::entities::TextFilterPB; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::type_options::translate_type_option::translate_entities::TranslateCellData; +use crate::services::field::type_options::util::ProtobufStr; +use crate::services::field::{ + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, +}; +use crate::services::sort::SortCondition; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; +use std::cmp::Ordering; + +#[derive(Debug, Clone)] +pub struct TranslateTypeOption { + pub auto_fill: bool, + /// Use [TranslateTypeOption::language_from_type] to get the language name + pub language_type: i64, +} + +impl TranslateTypeOption { + pub fn language_from_type(language_type: i64) -> &'static str { + match language_type { + 0 => "Chinese", + 1 => "English", + 2 => "French", + 3 => "German", + _ => "English", + } + } +} + +impl Default for TranslateTypeOption { + fn default() -> Self { + Self { + auto_fill: false, + language_type: 1, + } + } +} + +impl From for TranslateTypeOption { + fn from(value: TypeOptionData) -> Self { + let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); + let language = value.get_i64_value("language").unwrap_or_default(); + Self { + auto_fill, + language_type: language, + } + } +} + +impl From for TypeOptionData { + fn from(value: TranslateTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_bool_value("auto_fill", value.auto_fill) + .insert_i64_value("language", value.language_type) + .build() + } +} + +impl TypeOption for TranslateTypeOption { + type CellData = TranslateCellData; + type CellChangeset = String; + type CellProtobufType = ProtobufStr; + type CellFilter = TextFilterPB; +} + +impl CellDataChangeset for TranslateTypeOption { + fn apply_changeset( + &self, + changeset: String, + _cell: Option, + ) -> FlowyResult<(Cell, TranslateCellData)> { + let cell_data = TranslateCellData(changeset); + Ok((cell_data.clone().into(), cell_data)) + } +} + +impl TypeOptionCellDataFilter for TranslateTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + cell_data: &::CellData, + ) -> bool { + filter.is_visible(cell_data) + } +} + +impl TypeOptionCellDataCompare for TranslateTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + sort_condition: SortCondition, + ) -> Ordering { + match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) => { + let order = cell_data.0.cmp(&other_cell_data.0); + sort_condition.evaluate_order(order) + }, + } + } +} + +impl CellDataDecoder for TranslateTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult { + Ok(TranslateCellData::from(cell)) + } + + fn stringify_cell_data(&self, cell_data: TranslateCellData) -> String { + cell_data.to_string() + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } +} +impl TypeOptionTransform for TranslateTypeOption {} + +impl TypeOptionCellDataSerde for TranslateTypeOption { + fn protobuf_encode( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + ProtobufStr::from(cell_data.0) + } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(TranslateCellData::from(cell)) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs new file mode 100644 index 0000000000..b52b746ab5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs @@ -0,0 +1,46 @@ +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +#[derive(Default, Debug, Clone)] +pub struct TranslateCellData(pub String); +impl std::ops::Deref for TranslateCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TypeOptionCellData for TranslateCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From<&Cell> for TranslateCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + } +} + +impl From for Cell { + fn from(data: TranslateCellData) -> Self { + new_cell_builder(FieldType::Translate) + .insert_str_value(CELL_DATA, data.0) + .build() + } +} + +impl ToString for TranslateCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef for TranslateCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index e86c4fd7e8..c283d39bbc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -11,11 +11,13 @@ use flowy_error::FlowyResult; use crate::entities::{ CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB, - SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB, + SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, + TranslateTypeOptionPB, URLTypeOptionPB, }; use crate::services::cell::CellDataDecoder; use crate::services::field::checklist_type_option::ChecklistTypeOption; use crate::services::field::summary_type_option::summary::SummarizationTypeOption; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption, @@ -185,6 +187,9 @@ pub fn type_option_data_from_pb>( FieldType::Summary => { SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into()) }, + FieldType::Translate => { + TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into()) + }, } } @@ -252,6 +257,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> .try_into() .unwrap() }, + FieldType::Translate => { + let translate_type_option: TranslateTypeOption = type_option.into(); + TranslateTypeOptionPB::from(translate_type_option) + .try_into() + .unwrap() + }, } } @@ -272,5 +283,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa FieldType::Checklist => ChecklistTypeOption.into(), FieldType::Relation => RelationTypeOption::default().into(), FieldType::Summary => SummarizationTypeOption::default().into(), + FieldType::Translate => TranslateTypeOption::default().into(), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 7e145bffb7..19f7faf31b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -11,6 +11,7 @@ use lib_infra::box_any::BoxAny; use crate::entities::FieldType; use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob}; use crate::services::field::summary_type_option::summary::SummarizationTypeOption; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; use crate::services::field::{ CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption, @@ -449,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> { self.cell_data_cache.clone(), ) }), + FieldType::Translate => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + field_type, + self.cell_data_cache.clone(), + ) + }), } } @@ -552,6 +563,9 @@ fn get_type_option_transform_handler( }, FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)) as Box, + FieldType::Translate => { + Box::new(TranslateTypeOption::from(type_option_data)) as Box + }, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 6a974cc3d5..3b7d6444ef 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -281,6 +281,7 @@ impl FilterInner { FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)), FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)), FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)), + FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)), }; FilterInner::Data { @@ -367,6 +368,10 @@ impl<'a> From<&'a Filter> for FilterMap { let filter = condition_and_content.cloned::()?; (filter.condition as u8, filter.content) }, + FieldType::Translate => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, filter.content) + }, }; Some((condition, content)) }; diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index 44657d8c23..70d3cd77a3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -134,6 +134,7 @@ pub fn make_test_board() -> DatabaseData { .build(); fields.push(relation_field); }, + FieldType::Translate => {}, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 6ef8d08c3a..b73f3bf92e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -132,6 +132,7 @@ pub fn make_test_grid() -> DatabaseData { .build(); fields.push(relation_field); }, + FieldType::Translate => {}, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index a54fd17996..5297ff14de 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -83,6 +83,7 @@ async fn export_and_then_import_meta_csv_test() { FieldType::CreatedTime => {}, FieldType::Relation => {}, FieldType::Summary => {}, + FieldType::Translate => {}, } } else { panic!( @@ -166,6 +167,7 @@ async fn history_database_import_test() { FieldType::CreatedTime => {}, FieldType::Relation => {}, FieldType::Summary => {}, + FieldType::Translate => {}, } } else { panic!( diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index ea995b7b1e..f44a82ad3e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,5 +1,7 @@ use anyhow::Error; -use client_api::entity::ai_dto::{SummarizeRowData, SummarizeRowParams}; +use client_api::entity::ai_dto::{ + SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams, +}; use client_api::entity::QueryCollabResult::{Failed, Success}; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; @@ -12,6 +14,7 @@ use tracing::{error, instrument}; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use lib_infra::future::FutureResult; @@ -139,4 +142,26 @@ where Ok(data.text) }) } + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult { + let language = language.to_string(); + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let data = TranslateRowData { + cells: translate_row, + language, + include_header: false, + }; + + let params = TranslateRowParams { workspace_id, data }; + let data = try_get_client?.translate_row(params).await?; + Ok(data) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 71fc99b465..6c923d7d89 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -6,6 +6,7 @@ use yrs::{Any, MapPrelim}; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use lib_infra::future::FutureResult; @@ -85,4 +86,14 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { // TODO(lucas): local ai FutureResult::new(async move { Ok("".to_string()) }) } + + fn translate_database_row( + &self, + _workspace_id: &str, + _translate_row: TranslateRowContent, + _language: &str, + ) -> FutureResult { + // TODO(lucas): local ai + FutureResult::new(async move { Ok(TranslateRowResponse::default()) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index 9e7dd7765d..afba36d585 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -4,6 +4,7 @@ use tokio::sync::oneshot::channel; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, }; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; @@ -105,4 +106,13 @@ where ) -> FutureResult { FutureResult::new(async move { Ok("".to_string()) }) } + + fn translate_database_row( + &self, + _workspace_id: &str, + _translate_row: TranslateRowContent, + _language: &str, + ) -> FutureResult { + FutureResult::new(async move { Ok(TranslateRowResponse::default()) }) + } }