diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index 2f7b2f1cf0..a43c32e849 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -45,7 +45,8 @@ "medium": "medium", "large": "large", "fontSize": "Font Size", - "import": "Import" + "import": "Import", + "moreOptions": "More options" }, "disclosureAction": { "rename": "Rename", @@ -108,6 +109,7 @@ "openAsPage": "Open as a Page", "addNewRow": "Add a new row", "openMenu": "Click to open menu", + "dragRow": "Long press to reorder the row", "viewDataBase": "View database", "referencePage": "This {name} is referenced" }, @@ -358,12 +360,14 @@ "autoGeneratorGenerate": "Generate", "autoGeneratorHintText": "Ask OpenAI ...", "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key", + "autoGeneratorRewrite": "Rewrite", "smartEdit": "AI Assistants", "openAI": "OpenAI", "smartEditFixSpelling": "Fix spelling", "warning": "⚠️ AI responses can be inaccurate or misleading.", "smartEditSummarize": "Summarize", - "smartEditImproveWriting": "Improve Writing", + "smartEditImproveWriting": "Improve writing", + "smartEditMakeLonger": "Make longer", "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", "smartEditDisabled": "Connect OpenAI in Settings", @@ -427,4 +431,4 @@ "emptyNoDate": "No unscheduled events" } } -} \ No newline at end of file +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart index 687a37871e..2604187381 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart @@ -44,7 +44,6 @@ class DateCellDataPersistence implements CellDataPersistence { @override Future> save(DateCellData data) { var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId); - if (data.dateTime != null) { final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString(); payload.date = date; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 4824d68a74..b2afde89b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -176,18 +176,28 @@ class DatabaseController { ); } - Future> moveRow({ + Future> moveGroupRow({ required RowPB fromRow, required String groupId, RowPB? toRow, }) { - return _databaseViewBackendSvc.moveRow( + return _databaseViewBackendSvc.moveGroupRow( fromRowId: fromRow.id, toGroupId: groupId, toRowId: toRow?.id, ); } + Future> moveRow({ + required RowPB fromRow, + required RowPB toRow, + }) { + return _databaseViewBackendSvc.moveRow( + fromRowId: fromRow.id, + toRowId: toRow.id, + ); + } + Future> moveGroup({ required String fromGroupId, required String toGroupId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 548450966a..0925190a0a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -43,7 +43,7 @@ class DatabaseViewBackendService { return DatabaseEventCreateRow(payload).send(); } - Future> moveRow({ + Future> moveGroupRow({ required RowId fromRowId, required String toGroupId, RowId? toRowId, @@ -60,6 +60,18 @@ class DatabaseViewBackendService { return DatabaseEventMoveGroupRow(payload).send(); } + Future> moveRow({ + required String fromRowId, + required String toRowId, + }) { + var payload = MoveRowPayloadPB.create() + ..viewId = viewId + ..fromRowId = fromRowId + ..toRowId = toRowId; + + return DatabaseEventMoveRow(payload).send(); + } + Future> moveGroup({ required String fromGroupId, required String toGroupId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 18aab93baf..59a745d899 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -30,7 +30,7 @@ abstract class RowCacheDelegate { class RowCache { final String viewId; - /// _rows containers the current block's rows + /// _rows contains the current block's rows /// Use List to reverse the order of the GridRow. final RowList _rowList = RowList(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 906513deab..a3f0717f63 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -54,7 +54,7 @@ class BoardBloc extends Bloc { final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex); final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); if (fromRow != null) { - _databaseController.moveRow( + _databaseController.moveGroupRow( fromRow: fromRow, toRow: toRow, groupId: groupId, @@ -70,7 +70,7 @@ class BoardBloc extends Bloc { final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex); final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex); if (fromRow != null) { - _databaseController.moveRow( + _databaseController.moveGroupRow( fromRow: fromRow, toRow: toRow, groupId: toGroupId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart index 0538e52673..6a7afc9b26 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -161,6 +161,9 @@ class CalendarDayCard extends StatelessWidget { }); renderHook.addSelectOptionHook((selectedOptions, cardData, _) { + if (selectedOptions.isEmpty) { + return const SizedBox.shrink(); + } final children = selectedOptions.map( (option) { return SelectOptionTag.fromOption( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index e4d3ccd742..77cf2fbe67 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -35,6 +35,17 @@ class GridBloc extends Bloc { ); await rowService.deleteRow(rowInfo.rowPB.id); }, + moveRow: (int from, int to) { + final List rows = [...state.rowInfos]; + + final fromRow = rows[from].rowPB; + final toRow = rows[to].rowPB; + + rows.insert(to, rows.removeAt(from)); + emit(state.copyWith(rowInfos: rows)); + + databaseController.moveRow(fromRow: fromRow, toRow: toRow); + }, didReceiveGridUpdate: (grid) { emit(state.copyWith(grid: Some(grid))); }, @@ -110,6 +121,7 @@ class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; + const factory GridEvent.moveRow(int from, int to) = _MoveRow; const factory GridEvent.didReceiveRowUpdate( List rows, RowsChangedReason listState, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 8497c89de4..d62cf6f6ca 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -91,21 +91,6 @@ class _GridPageState extends State { ), ); } - - @override - void dispose() { - super.dispose(); - } - - @override - void deactivate() { - super.deactivate(); - } - - @override - void didUpdateWidget(covariant GridPage oldWidget) { - super.didUpdateWidget(oldWidget); - } } class FlowyGrid extends StatefulWidget { @@ -119,12 +104,12 @@ class _FlowyGridState extends State { final _scrollController = GridScrollController( scrollGroupController: LinkedScrollControllerGroup(), ); - late ScrollController headerScrollController; + late final ScrollController headerScrollController; @override void initState() { - headerScrollController = _scrollController.linkHorizontalController(); super.initState(); + headerScrollController = _scrollController.linkHorizontalController(); } @override @@ -216,49 +201,78 @@ class _GridRowsState extends State<_GridRows> { @override Widget build(BuildContext context) { - return BlocConsumer( - listenWhen: (previous, current) => previous.reason != current.reason, - listener: (context, state) { - state.reason.whenOrNull( - insert: (item) { - _key.currentState?.insertItem(item.index); - }, - delete: (item) { - _key.currentState?.removeItem( - item.index, - (context, animation) => - _renderRow(context, item.rowInfo, animation), + return Builder( + builder: (context) { + final filterState = context.watch().state; + final sortState = context.watch().state; + + return BlocConsumer( + listenWhen: (previous, current) => previous.reason != current.reason, + listener: (context, state) { + state.reason.whenOrNull( + insert: (item) { + _key.currentState?.insertItem(item.index); + }, + delete: (item) { + _key.currentState?.removeItem( + item.index, + (context, animation) => _renderRow( + context, + item.rowInfo, + animation: animation, + ), + ); + }, ); }, - reorderSingleRow: (reorderRow, rowInfo) { - // _key.currentState?.removeItem( - // reorderRow.oldIndex, - // (context, animation) => _renderRow(context, rowInfo, animation), - // ); - // _key.currentState?.insertItem(reorderRow.newIndex); - }, - ); - }, - buildWhen: (previous, current) { - return current.reason.whenOrNull( + buildWhen: (previous, current) { + return current.reason.maybeWhen( reorderRows: () => true, reorderSingleRow: (reorderRow, rowInfo) => true, - ) ?? - false; - }, - builder: (context, state) { - return SliverAnimatedList( - key: _key, - initialItemCount: context.read().state.rowInfos.length, - itemBuilder: - (BuildContext context, int index, Animation animation) { - final rowInfos = context.read().state.rowInfos; - if (index >= rowInfos.length) { - return const SizedBox(); - } else { - final RowInfo rowInfo = rowInfos[index]; - return _renderRow(context, rowInfo, animation); - } + delete: (item) => true, + insert: (item) => true, + orElse: () => false, + ); + }, + builder: (context, state) { + final rowInfos = context.watch().state.rowInfos; + + return SliverFillRemaining( + child: ReorderableListView.builder( + key: _key, + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity( + opacity: .5, + child: child, + ), + ), + onReorder: (fromIndex, newIndex) { + final toIndex = + newIndex > fromIndex ? newIndex - 1 : newIndex; + + if (fromIndex == toIndex) { + return; + } + + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + }, + itemCount: rowInfos.length, + itemBuilder: (BuildContext context, int index) { + final RowInfo rowInfo = rowInfos[index]; + return _renderRow( + context, + rowInfo, + index: index, + isSortEnabled: sortState.sortInfos.isNotEmpty, + isFilterEnabled: filterState.filters.isNotEmpty, + ); + }, + ), + ); }, ); }, @@ -267,15 +281,18 @@ class _GridRowsState extends State<_GridRows> { Widget _renderRow( BuildContext context, - RowInfo rowInfo, - Animation animation, - ) { + RowInfo rowInfo, { + int? index, + bool isSortEnabled = false, + bool isFilterEnabled = false, + Animation? animation, + }) { final rowCache = context.read().getRowCache( rowInfo.rowPB.id, ); /// Return placeholder widget if the rowCache is null. - if (rowCache == null) return const SizedBox(); + if (rowCache == null) return const SizedBox.shrink(); final fieldController = context.read().databaseController.fieldController; @@ -285,24 +302,32 @@ class _GridRowsState extends State<_GridRows> { rowCache: rowCache, ); - return SizeTransition( - sizeFactor: animation, - child: GridRow( - rowInfo: rowInfo, - dataController: dataController, - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), - openDetailPage: (context, cellBuilder) { - _openRowDetailPage( - context, - rowInfo, - fieldController, - rowCache, - cellBuilder, - ); - }, - key: ValueKey(rowInfo.rowPB.id), - ), + final child = GridRow( + key: ValueKey(rowInfo.rowPB.id), + index: index, + isDraggable: !isSortEnabled && !isFilterEnabled, + rowInfo: rowInfo, + dataController: dataController, + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + openDetailPage: (context, cellBuilder) { + _openRowDetailPage( + context, + rowInfo, + fieldController, + rowCache, + cellBuilder, + ); + }, ); + + if (animation != null) { + return SizeTransition( + sizeFactor: animation, + child: child, + ); + } + + return child; } void _openRowDetailPage( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index 52ba6fccb5..5fd0169c39 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -25,29 +25,34 @@ class GridRow extends StatefulWidget { final GridCellBuilder cellBuilder; final void Function(BuildContext, GridCellBuilder) openDetailPage; + final int? index; + final bool isDraggable; + const GridRow({ + super.key, required this.rowInfo, required this.dataController, required this.cellBuilder, required this.openDetailPage, - Key? key, - }) : super(key: key); + this.index, + this.isDraggable = false, + }); @override State createState() => _GridRowState(); } class _GridRowState extends State { - late RowBloc _rowBloc; + late final RowBloc _rowBloc; @override void initState() { + super.initState(); _rowBloc = RowBloc( rowInfo: widget.rowInfo, dataController: widget.dataController, ); _rowBloc.add(const RowEvent.initial()); - super.initState(); } @override @@ -70,9 +75,11 @@ class _GridRowState extends State { return Row( children: [ - const _RowLeading(), + _RowLeading( + index: widget.index, + isDraggable: widget.isDraggable, + ), content, - const _RowTrailing(), ], ); }, @@ -89,19 +96,25 @@ class _GridRowState extends State { } class _RowLeading extends StatefulWidget { - const _RowLeading({Key? key}) : super(key: key); + final int? index; + final bool isDraggable; + + const _RowLeading({ + this.index, + this.isDraggable = false, + }); @override State<_RowLeading> createState() => _RowLeadingState(); } class _RowLeadingState extends State<_RowLeading> { - late PopoverController popoverController; + late final PopoverController popoverController; @override void initState() { - popoverController = PopoverController(); super.initState(); + popoverController = PopoverController(); } @override @@ -131,23 +144,28 @@ class _RowLeadingState extends State<_RowLeading> { mainAxisAlignment: MainAxisAlignment.center, children: [ const _InsertButton(), - _MenuButton( - openMenu: () { - popoverController.show(); - }, - ), + if (isDraggable) ...[ + ReorderableDragStartListener( + index: widget.index!, + child: _MenuButton( + isDragEnabled: isDraggable, + openMenu: () { + popoverController.show(); + }, + ), + ), + ] else ...[ + _MenuButton( + openMenu: () { + popoverController.show(); + }, + ), + ], ], ); } -} -class _RowTrailing extends StatelessWidget { - const _RowTrailing({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const SizedBox(); - } + bool get isDraggable => widget.index != null && widget.isDraggable; } class _InsertButton extends StatelessWidget { @@ -172,25 +190,31 @@ class _InsertButton extends StatelessWidget { class _MenuButton extends StatefulWidget { final VoidCallback openMenu; + final bool isDragEnabled; + const _MenuButton({ required this.openMenu, - Key? key, - }) : super(key: key); + this.isDragEnabled = false, + }); @override State<_MenuButton> createState() => _MenuButtonState(); } class _MenuButtonState extends State<_MenuButton> { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return FlowyIconButton( - tooltipText: LocaleKeys.tooltip_openMenu.tr(), + tooltipText: + widget.isDragEnabled ? null : LocaleKeys.tooltip_openMenu.tr(), + richTooltipText: widget.isDragEnabled + ? TextSpan( + children: [ + TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'), + TextSpan(text: LocaleKeys.tooltip_openMenu.tr()), + ], + ) + : null, hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 20, height: 30, @@ -258,6 +282,7 @@ class RowContent extends StatelessWidget { if (builder != null) { accessories.addAll(builder(buildContext)); } + return accessories; }, child: child, @@ -289,12 +314,12 @@ class _RowEnterRegion extends StatefulWidget { } class _RowEnterRegionState extends State<_RowEnterRegion> { - late RegionStateNotifier _rowStateNotifier; + late final RegionStateNotifier _rowStateNotifier; @override void initState() { - _rowStateNotifier = RegionStateNotifier(); super.initState(); + _rowStateNotifier = RegionStateNotifier(); } @override diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index c4dd4adae9..415e884d29 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -74,6 +74,7 @@ class GridCellBuilder { key: key, ); } + throw UnimplementedError; } } @@ -83,7 +84,7 @@ class BlankCell extends StatelessWidget { @override Widget build(BuildContext context) { - return Container(); + return const SizedBox.shrink(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart index 5fb04100b0..54d3d56a8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart @@ -68,18 +68,15 @@ class CellContainer extends StatelessWidget { if (isFocus) { final borderSide = BorderSide( color: Theme.of(context).colorScheme.primary, - width: 1.0, ); + return BoxDecoration(border: Border.fromBorderSide(borderSide)); - } else { - final borderSide = BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0, - ); - return BoxDecoration( - border: Border(right: borderSide, bottom: borderSide), - ); } + + final borderSide = BorderSide(color: Theme.of(context).dividerColor); + return BoxDecoration( + border: Border(right: borderSide, bottom: borderSide), + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 799de68844..fd4f129a51 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -82,7 +82,6 @@ class DateCellCalendarBloc date != null && time == null, ); String? newTime = time ?? state.time; - DateTime? newDate = _utcToLocalAddTime(date); if (time != null && time.isNotEmpty) { newDate = state.dateTime ?? DateTime.now(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart index 2e4a05b87b..9c67de7493 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart @@ -6,7 +6,8 @@ import 'package:easy_localization/easy_localization.dart'; enum SmartEditAction { summarize, fixSpelling, - improveWriting; + improveWriting, + makeItLonger; String get toInstruction { switch (this) { @@ -16,6 +17,8 @@ enum SmartEditAction { return 'Correct this to standard English:'; case SmartEditAction.improveWriting: return 'Rewrite this in your own words:'; + case SmartEditAction.makeItLonger: + return 'Make this text longer:'; } } @@ -27,6 +30,8 @@ enum SmartEditAction { return 'Correct this to standard English:\n\n$input'; case SmartEditAction.improveWriting: return 'Rewrite this:\n\n$input'; + case SmartEditAction.makeItLonger: + return 'Make this text longer:\n\n$input'; } } @@ -38,6 +43,8 @@ enum SmartEditAction { return SmartEditAction.fixSpelling; case 2: return SmartEditAction.improveWriting; + case 3: + return SmartEditAction.makeItLonger; } return SmartEditAction.fixSpelling; } @@ -50,6 +57,8 @@ enum SmartEditAction { return LocaleKeys.document_plugins_smartEditFixSpelling.tr(); case SmartEditAction.improveWriting: return LocaleKeys.document_plugins_smartEditImproveWriting.tr(); + case SmartEditAction.makeItLonger: + return LocaleKeys.document_plugins_smartEditMakeLonger.tr(); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart index 50057bcfa5..fdbe5ba157 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart @@ -257,15 +257,15 @@ class _SmartEditInputWidgetState extends State { } Widget _buildResultWidget(BuildContext context) { - final loading = Padding( + final loadingWidget = Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: SizedBox.fromSize( size: const Size.square(14), child: const CircularProgressIndicator(), ), ); - if (result.isEmpty) { - return loading; + if (result.isEmpty || loading) { + return loadingWidget; } return Flexible( child: Text( @@ -277,6 +277,18 @@ class _SmartEditInputWidgetState extends State { Widget _buildInputFooterWidget(BuildContext context) { return Row( children: [ + FlowyRichTextButton( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + onPressed: () => _requestCompletions(rewrite: true), + ), + const Space(10, 0), FlowyRichTextButton( TextSpan( children: [ @@ -402,7 +414,13 @@ class _SmartEditInputWidgetState extends State { ); } - Future _requestCompletions() async { + Future _requestCompletions({bool rewrite = false}) async { + if (rewrite) { + setState(() { + loading = true; + result = ""; + }); + } final openAIRepository = await getIt.getAsync(); var lines = content.split('\n\n'); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart index 072b5bf195..181ac2b225 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart @@ -1,5 +1,7 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,6 +16,7 @@ class DocumentMoreButton extends StatelessWidget { return PopupMenuButton( color: Theme.of(context).colorScheme.surfaceVariant, offset: const Offset(0, 30), + tooltip: LocaleKeys.moreAction_moreOptions.tr(), itemBuilder: (context) { return [ PopupMenuItem( diff --git a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart new file mode 100644 index 0000000000..c4c0ca87aa --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart @@ -0,0 +1,11 @@ +import 'dart:ui'; + +class ColorGenerator { + Color generateColorFromString(String string) { + final hash = string.hashCode; + int r = (hash & 0xFF0000) >> 16; + int g = (hash & 0x00FF00) >> 8; + int b = hash & 0x0000FF; + return Color.fromRGBO(r, g, b, 0.5); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index 299d3fe03c..bc5b340b92 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -1,10 +1,10 @@ class HomeSizes { - static double get menuAddButtonHeight => 60; - static double get topBarHeight => 60; - static double get editPanelTopBarHeight => 60; - static double get editPanelWidth => 400; + static const double menuAddButtonHeight = 60; + static const double topBarHeight = 60; + static const double editPanelTopBarHeight = 60; + static const double editPanelWidth = 400; } class HomeInsets { - static double get topBarTitlePadding => 12; + static const double topBarTitlePadding = 12; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index e448243c49..176ff7ac7b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -170,23 +170,24 @@ class HomeStackManager { return MultiProvider( providers: [ChangeNotifierProvider.value(value: _notifier)], child: Consumer( - builder: (ctx, HomeStackNotifier notifier, child) { + builder: (_, HomeStackNotifier notifier, __) { return FadingIndexedStack( index: getIt().indexOf(notifier.plugin.ty), - children: - getIt().supportPluginTypes.map((pluginType) { - if (pluginType == notifier.plugin.ty) { - final pluginWidget = notifier.plugin.display - .buildWidget(PluginContext(onDeleted: onDeleted)); - if (pluginType == PluginType.editor) { - return pluginWidget; - } else { + children: getIt().supportPluginTypes.map( + (pluginType) { + if (pluginType == notifier.plugin.ty) { + final pluginWidget = notifier.plugin.display + .buildWidget(PluginContext(onDeleted: onDeleted)); + if (pluginType == PluginType.editor) { + return pluginWidget; + } + return pluginWidget.padding(horizontal: 40, vertical: 28); } - } else { + return const BlankPage(); - } - }).toList(), + }, + ).toList(), ); }, ), @@ -204,30 +205,27 @@ class HomeTopBar extends StatelessWidget { return Container( color: Theme.of(context).colorScheme.onSecondaryContainer, height: HomeSizes.topBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - HSpace(layout.menuSpacing), - const FlowyNavigation(), - const HSpace(16), - ChangeNotifierProvider.value( - value: Provider.of(context, listen: false), - child: Consumer( - builder: ( - BuildContext context, - HomeStackNotifier notifier, - Widget? child, - ) { - return notifier.plugin.display.rightBarItem ?? const SizedBox(); - }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: HomeInsets.topBarTitlePadding, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + HSpace(layout.menuSpacing), + const FlowyNavigation(), + const HSpace(16), + ChangeNotifierProvider.value( + value: Provider.of(context, listen: false), + child: Consumer( + builder: (_, HomeStackNotifier notifier, __) => + notifier.plugin.display.rightBarItem ?? + const SizedBox.shrink(), + ), ), - ) // _renderMoreButton(), - ], - ) - .padding( - horizontal: HomeInsets.topBarTitlePadding, - ) - .bottomBorder(color: Theme.of(context).dividerColor), + ], + ).bottomBorder(color: Theme.of(context).dividerColor), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart index c75f8372a7..988c482940 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart @@ -1,4 +1,5 @@ import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; @@ -45,8 +46,31 @@ class MenuUser extends StatelessWidget { String iconUrl = context.read().state.userProfile.iconUrl; if (iconUrl.isEmpty) { iconUrl = defaultUserAvatar; + final String name = context.read().state.userProfile.name; + final Color color = ColorGenerator().generateColorFromString(name); + const initialsCount = 2; + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = name + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(''); + return Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: FlowyText.semibold( + nameInitials, + color: Colors.white, + fontSize: nameInitials.length == initialsCount ? 12 : 14, + ), + ); } - return SizedBox( width: 25, height: 25, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart index 94cd80c269..b3885a8ee7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -17,12 +17,13 @@ class ViewLeftBarItem extends StatefulWidget { class _ViewLeftBarItemState extends State { final _controller = TextEditingController(); final _focusNode = FocusNode(); - late ViewService _viewService; - late ViewListener _viewListener; + late final ViewService _viewService; + late final ViewListener _viewListener; late ViewPB view; @override void initState() { + super.initState(); view = widget.view; _viewService = ViewService(); _focusNode.addListener(_handleFocusChanged); @@ -39,7 +40,8 @@ class _ViewLeftBarItemState extends State { ); }, ); - super.initState(); + + _controller.text = view.name; } @override @@ -53,30 +55,24 @@ class _ViewLeftBarItemState extends State { @override Widget build(BuildContext context) { - _controller.text = view.name; - - return IntrinsicWidth( + return GestureDetector( key: ValueKey(_controller.text), - child: GestureDetector( - onDoubleTap: () { - _controller.selection = TextSelection( - baseOffset: 0, - extentOffset: _controller.text.length, - ); - }, - child: TextField( - controller: _controller, - focusNode: _focusNode, - scrollPadding: EdgeInsets.zero, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 4.0), - border: InputBorder.none, - isDense: true, - ), - style: Theme.of(context).textTheme.bodyMedium, - // cursorColor: widget.cursorColor, - // obscureText: widget.enableObscure, + onDoubleTap: () { + _controller.selection = TextSelection( + baseOffset: 0, + extentOffset: _controller.text.length, + ); + }, + child: TextField( + controller: _controller, + focusNode: _focusNode, + scrollPadding: EdgeInsets.zero, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 4.0), + border: InputBorder.none, + isDense: true, ), + style: Theme.of(context).textTheme.bodyMedium, ), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index d35e07a954..644957e82d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -38,7 +38,7 @@ String languageFromLocale(Locale locale) { case "hu": return "Magyar"; case "id": - return "Bahasa"; + return "Bahasa Indonesia"; case "it": return "Italiano"; case "ja": diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart index c41d3fc750..04ee12d4f3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart @@ -3,7 +3,7 @@ export 'package:styled_widget/styled_widget.dart'; extension FlowyStyledWidget on Widget { Widget bottomBorder({double width = 1.0, Color color = Colors.grey}) { - return Container( + return DecoratedBox( decoration: BoxDecoration( border: Border( bottom: BorderSide(width: width, color: color), @@ -14,7 +14,7 @@ extension FlowyStyledWidget on Widget { } Widget topBorder({double width = 1.0, Color color = Colors.grey}) { - return Container( + return DecoratedBox( decoration: BoxDecoration( border: Border( top: BorderSide(width: width, color: color), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index 0e124e06c1..c8d089799e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -16,6 +16,7 @@ class FlowyIconButton extends StatelessWidget { final EdgeInsets iconPadding; final BorderRadius? radius; final String? tooltipText; + final InlineSpan? richTooltipText; final bool preferBelow; const FlowyIconButton({ @@ -29,15 +30,22 @@ class FlowyIconButton extends StatelessWidget { this.iconPadding = EdgeInsets.zero, this.radius, this.tooltipText, + this.richTooltipText, this.preferBelow = true, required this.icon, - }) : super(key: key); + }) : assert((richTooltipText != null && tooltipText == null) || + (richTooltipText == null && tooltipText != null) || + (richTooltipText == null && tooltipText == null)), + super(key: key); @override Widget build(BuildContext context) { Widget child = icon; final size = Size(width, height ?? width); + final tooltipMessage = + tooltipText == null && richTooltipText == null ? '' : tooltipText; + assert(size.width > iconPadding.horizontal); assert(size.height > iconPadding.vertical); @@ -46,11 +54,14 @@ class FlowyIconButton extends StatelessWidget { final childSize = Size(childWidth, childWidth); return ConstrainedBox( - constraints: - BoxConstraints.tightFor(width: size.width, height: size.height), + constraints: BoxConstraints.tightFor( + width: size.width, + height: size.height, + ), child: Tooltip( preferBelow: preferBelow, - message: tooltipText ?? '', + message: tooltipMessage, + richMessage: richTooltipText, showDuration: Duration.zero, child: RawMaterialButton( visualDensity: VisualDensity.compact, diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart index c2242bb14f..2d87efed74 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart @@ -13,9 +13,11 @@ void main() { group('Edit Grid:', () { late GridTestContext context; + setUp(() async { context = await gridTest.createTestGrid(); }); + // The initial number of rows is 3 for each grid. blocTest( "create a row", @@ -54,5 +56,34 @@ void main() { ); }, ); + + String? firstId; + String? secondId; + String? thirdId; + + blocTest( + 'reorder rows', + build: () => GridBloc( + view: context.gridView, + databaseController: DatabaseController( + view: context.gridView, + layoutType: DatabaseLayoutPB.Grid, + ), + )..add(const GridEvent.initial()), + act: (bloc) async { + await gridResponseFuture(); + + firstId = bloc.state.rowInfos[0].rowPB.id; + secondId = bloc.state.rowInfos[1].rowPB.id; + thirdId = bloc.state.rowInfos[2].rowPB.id; + + bloc.add(const GridEvent.moveRow(0, 2)); + }, + verify: (bloc) { + expect(secondId, bloc.state.rowInfos[0].rowPB.id); + expect(thirdId, bloc.state.rowInfos[1].rowPB.id); + expect(firstId, bloc.state.rowInfos[2].rowPB.id); + }, + ); }); } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs index 744b8f7f93..b265bc3f76 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs @@ -159,7 +159,7 @@ mod tests { date: Some("1653609600".to_owned()), time: Some("9:00 AM".to_owned()), include_time: Some(true), - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, None, "May 27, 2022 09:00 AM", @@ -213,7 +213,7 @@ mod tests { date: Some("1653609600".to_owned()), time: Some("1:".to_owned()), include_time: Some(true), - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, None, "May 27, 2022 01:00", @@ -252,7 +252,7 @@ mod tests { date: Some("1653609600".to_owned()), time: Some("00:00".to_owned()), include_time: Some(true), - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, None, "May 27, 2022 00:00", @@ -273,7 +273,7 @@ mod tests { date: Some("1653609600".to_owned()), time: Some("1:00 am".to_owned()), include_time: Some(true), - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, None, "May 27, 2022 01:00 AM", @@ -372,7 +372,7 @@ mod tests { date: Some("1700006400".to_owned()), time: Some("08:00".to_owned()), include_time: Some(true), - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, ); assert_date( @@ -382,7 +382,7 @@ mod tests { date: None, time: Some("14:00".to_owned()), include_time: None, - timezone_id: None, + timezone_id: Some("Etc/UTC".to_owned()), }, Some(old_cell_data), "Nov 15, 2023 14:00", diff --git a/inlang.config.js b/inlang.config.js index 0cf8ffc43c..69730af126 100644 --- a/inlang.config.js +++ b/inlang.config.js @@ -1,29 +1,17 @@ - -// @ts-check - -/** - * @type { import("@inlang/core/config").DefineConfig } - */ export async function defineConfig(env) { - const plugin = await env.$import( - "https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@1.1.1/dist/index.js" - ); + const { default: pluginJson } = await env.$import( + 'https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@2/dist/index.js' + ); - const { default: standardLintRules } = await env.$import( - "https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@1.1.1/dist/index.js" - ); + const { default: standardLintRules } = await env.$import( + 'https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@2/dist/index.js' + ); - const pluginConfig = { - pathPattern: "./frontend/appflowy_flutter/assets/translations/{language}.json", - }; - - return { - referenceLanguage: "en", - languages: await plugin.getLanguages({ ...env, pluginConfig }), - readResources: (args) => plugin.readResources({ ...args, ...env, pluginConfig }), - writeResources: (args) => plugin.writeResources({ ...args, ...env, pluginConfig }), - lint: { - rules: [standardLintRules()], - }, - }; -} \ No newline at end of file + return { + referenceLanguage: 'en', + plugins: [pluginJson({ + pathPattern: './frontend/appflowy_flutter/assets/translations/{language}.json', + variableReferencePattern: ["@:"] + }), standardLintRules()] + }; +}