mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: AI translation in Database (#5515)
* chore: add tranlate field type * chore: integrate ai translate * chore: integrate client api * chore: implement UI
This commit is contained in:
parent
815c99710e
commit
3d7a500550
@ -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
|
||||
|
@ -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<RoundedRectangleBorder>(
|
||||
|
@ -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<TranslateCellEvent, TranslateCellState> {
|
||||
TranslateCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(TranslateCellState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final TranslateCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(
|
||||
onCellChanged: _onCellChangedFn!,
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TranslateCellEvent>(
|
||||
(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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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<TranslateRowEvent, TranslateRowState> {
|
||||
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<TranslateRowEvent>(
|
||||
(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<void, FlowyError> 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;
|
||||
}
|
@ -16,6 +16,7 @@ typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
typedef RelationCellController = CellController<RelationCellDataPB, String>;
|
||||
typedef SummaryCellController = CellController<String, String>;
|
||||
typedef TranslateCellController = CellController<String, String>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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<TranslateTypeOptionEvent, TranslateTypeOptionState> {
|
||||
TranslateTypeOptionBloc({required TranslateTypeOptionPB option})
|
||||
: super(TranslateTypeOptionState.initial(option)) {
|
||||
on<TranslateTypeOptionEvent>(
|
||||
(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';
|
||||
}
|
||||
}
|
@ -4,8 +4,6 @@ abstract class TypeOptionParser<T> {
|
||||
T fromBuffer(List<int> buffer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
|
||||
@override
|
||||
NumberTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
@ -51,3 +49,11 @@ class RelationTypeOptionDataParser
|
||||
return RelationTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateTypeOptionDataParser
|
||||
extends TypeOptionParser<TranslateTypeOptionPB> {
|
||||
@override
|
||||
TranslateTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return TranslateTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -93,10 +93,13 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
onFieldInserted: widget.onFieldInsertedOnEitherSide,
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: FieldCellButton(
|
||||
field: widget.fieldInfo.field,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const line = Positioned(
|
||||
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -196,7 +196,10 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
@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),
|
||||
|
@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@ -98,6 +99,12 @@ class CardCellBuilder {
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Translate => TranslateCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
_ => throw UnimplementedError,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class TranslateCardCellStyle extends CardCellStyle {
|
||||
const TranslateCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
class TranslateCardCell extends CardCell<TranslateCardCellStyle> {
|
||||
const TranslateCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<TranslateCardCell> createState() => _TranslateCellState();
|
||||
}
|
||||
|
||||
class _TranslateCellState extends State<TranslateCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return TranslateCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<TranslateCellBloc, TranslateCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: widget.style.padding,
|
||||
child: Text(state.content, style: widget.style.textStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -84,5 +84,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -84,5 +84,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -83,5 +83,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
onExit: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
@ -69,9 +73,10 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 0),
|
||||
).positioned(right: 0, bottom: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,99 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => TranslateMouseNotifier(),
|
||||
builder: (context, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = true,
|
||||
onExit: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<TranslateMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
TranslateMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return TranslateCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateMouseNotifier extends ChangeNotifier {
|
||||
TranslateMouseNotifier();
|
||||
|
||||
bool _onEnter = false;
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TranslateCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@ -120,6 +121,12 @@ class EditableCellBuilder {
|
||||
skin: IEditableSummaryCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
FieldType.Translate => EditableTranslateCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: IEditableTranslateCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ class SummaryButton extends StatelessWidget {
|
||||
},
|
||||
finish: (_) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_genSummary.tr(),
|
||||
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
|
@ -0,0 +1,250 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableTranslateCellSkin {
|
||||
const IEditableTranslateCellSkin();
|
||||
|
||||
factory IEditableTranslateCellSkin.fromStyle(EditableCellStyle style) {
|
||||
return switch (style) {
|
||||
EditableCellStyle.desktopGrid => DesktopGridTranslateCellSkin(),
|
||||
EditableCellStyle.desktopRowDetail => DesktopRowDetailTranslateCellSkin(),
|
||||
EditableCellStyle.mobileGrid => MobileGridTranslateCellSkin(),
|
||||
EditableCellStyle.mobileRowDetail => MobileRowDetailTranslateCellSkin(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
);
|
||||
}
|
||||
|
||||
class EditableTranslateCell extends EditableCellWidget {
|
||||
EditableTranslateCell({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
required this.skin,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
final IEditableTranslateCellSkin skin;
|
||||
|
||||
@override
|
||||
GridEditableTextCell<EditableTranslateCell> createState() =>
|
||||
_TranslateCellState();
|
||||
}
|
||||
|
||||
class _TranslateCellState extends GridEditableTextCell<EditableTranslateCell> {
|
||||
late final TextEditingController _textEditingController;
|
||||
late final cellBloc = TranslateCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController =
|
||||
TextEditingController(text: cellBloc.state.content);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocListener<TranslateCellBloc, TranslateCellState>(
|
||||
listener: (context, state) {
|
||||
_textEditingController.text = state.content;
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
focusNode,
|
||||
_textEditingController,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void onRequestFocus() {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => cellBloc.state.content;
|
||||
|
||||
@override
|
||||
Future<void> focusChanged() {
|
||||
if (mounted &&
|
||||
!cellBloc.isClosed &&
|
||||
cellBloc.state.content != _textEditingController.text.trim()) {
|
||||
cellBloc.add(
|
||||
TranslateCellEvent.updateCell(_textEditingController.text.trim()),
|
||||
);
|
||||
}
|
||||
return super.focusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateCellAccessory extends StatelessWidget {
|
||||
const TranslateCellAccessory({
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.fieldId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
final String fieldId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => TranslateRowBloc(
|
||||
viewId: viewId,
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<TranslateRowBloc, TranslateRowState>(
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [TranslateButton(), HSpace(6), CopyButton()],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TranslateButton extends StatelessWidget {
|
||||
const TranslateButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TranslateRowBloc, TranslateRowState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
finish: (_) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_summary_generate_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<TranslateRowBloc>()
|
||||
.add(const TranslateRowEvent.startTranslate());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CopyButton extends StatelessWidget {
|
||||
const CopyButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TranslateCellBloc, TranslateCellState>(
|
||||
builder: (blocContext, state) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_clickToCopy.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_copy_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: state.content));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => TranslateMouseNotifier(),
|
||||
builder: (context, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = true,
|
||||
onExit: (p) =>
|
||||
Provider.of<TranslateMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<TranslateMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
TranslateMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return TranslateCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TranslateCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TranslateCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
FieldType.Translate,
|
||||
];
|
||||
|
||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -33,6 +34,7 @@ abstract class TypeOptionEditorFactory {
|
||||
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
|
||||
FieldType.Relation => const RelationTypeOptionEditorFactory(),
|
||||
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
|
||||
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,168 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/translate_type_option_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import './builder.dart';
|
||||
|
||||
class TranslateTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
const TranslateTypeOptionEditorFactory();
|
||||
|
||||
@override
|
||||
Widget? build({
|
||||
required BuildContext context,
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required PopoverMutex popoverMutex,
|
||||
required TypeOptionDataCallback onTypeOptionUpdated,
|
||||
}) {
|
||||
final typeOption = TranslateTypeOptionPB.fromBuffer(field.typeOptionData);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText(
|
||||
LocaleKeys.grid_field_translateTo.tr(),
|
||||
),
|
||||
const HSpace(6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: BlocProvider(
|
||||
create: (context) => TranslateTypeOptionBloc(option: typeOption),
|
||||
child: BlocConsumer<TranslateTypeOptionBloc,
|
||||
TranslateTypeOptionState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.option != current.option,
|
||||
listener: (context, state) {
|
||||
onTypeOptionUpdated(state.option.writeToBuffer());
|
||||
},
|
||||
builder: (context, state) {
|
||||
return _wrapLanguageListPopover(
|
||||
context,
|
||||
state,
|
||||
popoverMutex,
|
||||
SelectLanguageButton(
|
||||
language: state.language,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapLanguageListPopover(
|
||||
BuildContext blocContext,
|
||||
TranslateTypeOptionState state,
|
||||
PopoverMutex popoverMutex,
|
||||
Widget child,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
asBarrier: true,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(8, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
popupBuilder: (popoverContext) {
|
||||
return LanguageList(
|
||||
onSelected: (language) {
|
||||
blocContext
|
||||
.read<TranslateTypeOptionBloc>()
|
||||
.add(TranslateTypeOptionEvent.selectLanguage(language));
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
selectedLanguage: state.option.language,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectLanguageButton extends StatelessWidget {
|
||||
const SelectLanguageButton({required this.language, super.key});
|
||||
final String language;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: FlowyButton(text: FlowyText(language)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageList extends StatelessWidget {
|
||||
const LanguageList({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
required this.selectedLanguage,
|
||||
});
|
||||
|
||||
final Function(TranslateLanguagePB) onSelected;
|
||||
final TranslateLanguagePB selectedLanguage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = TranslateLanguagePB.values.map((languageType) {
|
||||
return LanguageCell(
|
||||
languageType: languageType,
|
||||
onSelected: onSelected,
|
||||
isSelected: languageType == selectedLanguage,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (context, index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
itemCount: cells.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return cells[index];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageCell extends StatelessWidget {
|
||||
const LanguageCell({
|
||||
required this.languageType,
|
||||
required this.onSelected,
|
||||
required this.isSelected,
|
||||
super.key,
|
||||
});
|
||||
final Function(TranslateLanguagePB) onSelected;
|
||||
final TranslateLanguagePB languageType;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget? checkmark;
|
||||
if (isSelected) {
|
||||
checkmark = const FlowySvg(FlowySvgs.check_s);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(languageTypeToLanguage(languageType)),
|
||||
rightIcon: checkmark,
|
||||
onTap: () => onSelected(languageType),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
23
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
23
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
41
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
41
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
||||
|
||||
|
||||
|
29
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
29
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
23
frontend/resources/flowy_icons/16x/ai_indicator.svg
Normal file
23
frontend/resources/flowy_icons/16x/ai_indicator.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_2389_7357)">
|
||||
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="url(#paint0_linear_2389_7357)" shape-rendering="crispEdges"/>
|
||||
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="#806989" shape-rendering="crispEdges"/>
|
||||
<path d="M11.1576 11.884H8.79963L8.42163 13H6.81063L9.09663 6.682H10.8786L13.1646 13H11.5356L11.1576 11.884ZM10.7616 10.696L9.97863 8.383L9.20463 10.696H10.7616ZM14.6794 7.456C14.4094 7.456 14.1874 7.378 14.0134 7.222C13.8454 7.06 13.7614 6.862 13.7614 6.628C13.7614 6.388 13.8454 6.19 14.0134 6.034C14.1874 5.872 14.4094 5.791 14.6794 5.791C14.9434 5.791 15.1594 5.872 15.3274 6.034C15.5014 6.19 15.5884 6.388 15.5884 6.628C15.5884 6.862 15.5014 7.06 15.3274 7.222C15.1594 7.378 14.9434 7.456 14.6794 7.456ZM15.4444 7.978V13H13.9054V7.978H15.4444Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_2389_7357" x="0.666626" y="0" width="22" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2389_7357"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2389_7357" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_2389_7357" x1="15.6666" y1="2.4" x2="6.86663" y2="17.2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#726084" stop-opacity="0.8"/>
|
||||
<stop offset="1" stop-color="#5D5862"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
3
frontend/resources/flowy_icons/16x/ai_keyword.svg
Normal file
3
frontend/resources/flowy_icons/16x/ai_keyword.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 2V7.33333H7.33333V2H2ZM6 6H3.33333V3.33333H6V6ZM2 8.66667V14H7.33333V8.66667H2ZM6 12.6667H3.33333V10H6V12.6667ZM8.66667 2V7.33333H14V2H8.66667ZM12.6667 6H10V3.33333H12.6667V6ZM8.66667 8.66667V14H14V8.66667H8.66667ZM12.6667 12.6667H10V10H12.6667V12.6667Z" fill="#750D7E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 387 B |
3
frontend/resources/flowy_icons/16x/ai_tag.svg
Normal file
3
frontend/resources/flowy_icons/16x/ai_tag.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.50584 14.2232C8.20974 14.519 7.85975 14.6668 7.45587 14.6668C7.05198 14.6668 6.70199 14.519 6.4059 14.2232L1.76115 9.58414C1.47589 9.29922 1.33325 8.95171 1.33325 8.54161C1.33325 8.13151 1.47589 7.784 1.76115 7.49909L7.47776 1.7771C7.61498 1.64004 7.77515 1.53185 7.95826 1.45251C8.14138 1.37317 8.33661 1.3335 8.54397 1.3335H13.1887C13.5952 1.3335 13.9431 1.47801 14.2325 1.76704C14.5219 2.05607 14.6666 2.40358 14.6666 2.80957V7.44866C14.6666 7.65577 14.6269 7.85077 14.5474 8.03366C14.468 8.21655 14.3597 8.37652 14.2224 8.51358L8.50584 14.2232ZM11.5647 5.40487C11.8354 5.40487 12.0654 5.31025 12.2549 5.12101C12.4444 4.93177 12.5391 4.70197 12.5391 4.43163C12.5391 4.16129 12.4444 3.9315 12.2549 3.74226C12.0654 3.55301 11.8354 3.45839 11.5647 3.45839C11.294 3.45839 11.0639 3.55301 10.8745 3.74226C10.685 3.9315 10.5903 4.16129 10.5903 4.43163C10.5903 4.70197 10.685 4.93177 10.8745 5.12101C11.0639 5.31025 11.294 5.40487 11.5647 5.40487ZM7.45199 13.1908L13.1887 7.44866V2.80957H8.54397L2.80724 8.55166L7.45199 13.1908Z" fill="#750D7E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
frontend/resources/flowy_icons/16x/ai_translate.svg
Normal file
3
frontend/resources/flowy_icons/16x/ai_translate.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.58008 10.0468L6.88675 8.3735L6.90675 8.3535C8.06675 7.06016 8.89341 5.5735 9.38008 4.00016H11.3334V2.66683H6.66675V1.3335H5.33341V2.66683H0.666748V3.9935H8.11341C7.66675 5.28016 6.96008 6.50016 6.00008 7.56683C5.38008 6.88016 4.86675 6.12683 4.46008 5.3335H3.12675C3.61341 6.42016 4.28008 7.44683 5.11341 8.3735L1.72008 11.7202L2.66675 12.6668L6.00008 9.3335L8.07341 11.4068L8.58008 10.0468ZM12.3334 6.66683H11.0001L8.00008 14.6668H9.33341L10.0801 12.6668H13.2467L14.0001 14.6668H15.3334L12.3334 6.66683ZM10.5867 11.3335L11.6667 8.44683L12.7467 11.3335H10.5867Z" fill="#750D7E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 695 B |
@ -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",
|
||||
|
45
frontend/rust-lib/Cargo.lock
generated
45
frontend/rust-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -1,2 +1,3 @@
|
||||
// mod summarize_row;
|
||||
// mod summarize_row_test;
|
||||
// mod translate_row_test;
|
||||
mod util;
|
||||
|
@ -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"));
|
||||
}
|
@ -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<Field> {
|
||||
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()
|
||||
}
|
||||
|
@ -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<TranslateRowResponse, Error> {
|
||||
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 {
|
||||
|
@ -10,3 +10,4 @@ lib-infra = { workspace = true }
|
||||
collab-entity = { workspace = true }
|
||||
collab = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
client-api = { workspace = true }
|
||||
|
@ -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<String, DataSource>;
|
||||
pub type SummaryRowContent = HashMap<String, String>;
|
||||
pub type TranslateRowContent = Vec<TranslateItem>;
|
||||
/// 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<String, Error>;
|
||||
|
||||
fn translate_database_row(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
translate_row: TranslateRowContent,
|
||||
language: &str,
|
||||
) -> FutureResult<TranslateRowResponse, Error>;
|
||||
}
|
||||
|
||||
pub struct DatabaseSnapshot {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
FieldType::Translate => condition_and_content
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -156,6 +160,9 @@ impl TryFrom<FilterDataPB> 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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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<TranslateTypeOption> for TranslateTypeOptionPB {
|
||||
fn from(value: TranslateTypeOption) -> Self {
|
||||
TranslateTypeOptionPB {
|
||||
auto_fill: value.auto_fill,
|
||||
language: value.language_type.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TranslateTypeOptionPB> 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<i64> for TranslateLanguagePB {
|
||||
fn from(data: i64) -> Self {
|
||||
match data {
|
||||
0 => TranslateLanguagePB::Chinese,
|
||||
1 => TranslateLanguagePB::English,
|
||||
2 => TranslateLanguagePB::French,
|
||||
3 => TranslateLanguagePB::German,
|
||||
_ => TranslateLanguagePB::English,
|
||||
}
|
||||
}
|
||||
}
|
@ -1104,3 +1104,16 @@ pub(crate) async fn summarize_row_handler(
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn translate_row_handler(
|
||||
data: AFPluginData<TranslateRowPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> 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,
|
||||
}
|
||||
|
@ -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::<Vec<String>>()
|
||||
.join(", ")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.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<dyn DatabaseCloudService> {
|
||||
|
@ -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));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
pub mod translate;
|
||||
pub mod translate_entities;
|
@ -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<TypeOptionData> 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<TranslateTypeOption> 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<Cell>,
|
||||
) -> FlowyResult<(Cell, TranslateCellData)> {
|
||||
let cell_data = TranslateCellData(changeset);
|
||||
Ok((cell_data.clone().into(), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for TranslateTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
filter: &<Self as TypeOption>::CellFilter,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> bool {
|
||||
filter.is_visible(cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for TranslateTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::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<TranslateCellData> {
|
||||
Ok(TranslateCellData::from(cell))
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: TranslateCellData) -> String {
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl TypeOptionTransform for TranslateTypeOption {}
|
||||
|
||||
impl TypeOptionCellDataSerde for TranslateTypeOption {
|
||||
fn protobuf_encode(
|
||||
&self,
|
||||
cell_data: <Self as TypeOption>::CellData,
|
||||
) -> <Self as TypeOption>::CellProtobufType {
|
||||
ProtobufStr::from(cell_data.0)
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(TranslateCellData::from(cell))
|
||||
}
|
||||
}
|
@ -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<TranslateCellData> 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<str> for TranslateCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -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<T: Into<Bytes>>(
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
@ -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::<TranslateTypeOption>(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<dyn TypeOptionTransformHandler>,
|
||||
FieldType::Translate => {
|
||||
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
FieldType::Translate => {
|
||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
};
|
||||
Some((condition, content))
|
||||
};
|
||||
|
@ -134,6 +134,7 @@ pub fn make_test_board() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Translate => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,6 +132,7 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Translate => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!(
|
||||
|
@ -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<TranslateRowResponse, Error> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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<TranslateRowResponse, Error> {
|
||||
// TODO(lucas): local ai
|
||||
FutureResult::new(async move { Ok(TranslateRowResponse::default()) })
|
||||
}
|
||||
}
|
||||
|
@ -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<String, Error> {
|
||||
FutureResult::new(async move { Ok("".to_string()) })
|
||||
}
|
||||
|
||||
fn translate_database_row(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_translate_row: TranslateRowContent,
|
||||
_language: &str,
|
||||
) -> FutureResult<TranslateRowResponse, Error> {
|
||||
FutureResult::new(async move { Ok(TranslateRowResponse::default()) })
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user