Merge remote-tracking branch 'origin/main' into enzoftware/main

# Conflicts:
#	frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
This commit is contained in:
Lucas.Xu 2022-10-08 10:15:30 +08:00
commit 74b43511d1
65 changed files with 971 additions and 287 deletions

View File

@ -1,4 +1,4 @@
name: FlowyEditor test name: AppFlowyEditor test
on: on:
push: push:
@ -37,6 +37,8 @@ jobs:
working-directory: frontend/app_flowy/packages/appflowy_editor working-directory: frontend/app_flowy/packages/appflowy_editor
run: | run: |
flutter pub get flutter pub get
flutter format --set-exit-if-changed .
flutter analyze .
flutter test --coverage flutter test --coverage
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3

View File

@ -45,5 +45,7 @@
<array> <array>
<string>en</string> <string>en</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -89,18 +89,30 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
(err) => Log.error(err), (err) => Log.error(err),
); );
}, },
didCreateRow: (String groupId, RowPB row, int? index) { didCreateRow: (group, row, int? index) {
emit(state.copyWith( emit(state.copyWith(
editingRow: Some(BoardEditingRow( editingRow: Some(BoardEditingRow(
columnId: groupId, group: group,
row: row, row: row,
index: index, index: index,
)), )),
)); ));
_groupItemStartEditing(group, row, true);
}, },
endEditRow: (rowId) { startEditingRow: (group, row) {
emit(state.copyWith(
editingRow: Some(BoardEditingRow(
group: group,
row: row,
index: null,
)),
));
_groupItemStartEditing(group, row, true);
},
endEditingRow: (rowId) {
state.editingRow.fold(() => null, (editingRow) { state.editingRow.fold(() => null, (editingRow) {
assert(editingRow.row.id == rowId); assert(editingRow.row.id == rowId);
_groupItemStartEditing(editingRow.group, editingRow.row, false);
emit(state.copyWith(editingRow: none())); emit(state.copyWith(editingRow: none()));
}); });
}, },
@ -122,6 +134,24 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
} }
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
final fieldContext = fieldController.getField(group.fieldId);
if (fieldContext == null) {
Log.warn("FieldContext should not be null");
return;
}
boardController.enableGroupDragging(!isEdit);
// boardController.updateGroupItem(
// group.groupId,
// GroupItem(
// row: row,
// fieldContext: fieldContext,
// isDraggable: !isEdit,
// ),
// );
}
void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) { void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
if (fromRow != null) { if (fromRow != null) {
_rowService _rowService
@ -136,11 +166,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
} }
} }
void _moveGroup(String fromColumnId, String toColumnId) { void _moveGroup(String fromGroupId, String toGroupId) {
_rowService _rowService
.moveGroup( .moveGroup(
fromGroupId: fromColumnId, fromGroupId: fromGroupId,
toGroupId: toColumnId, toGroupId: toGroupId,
) )
.then((result) { .then((result) {
result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r))); result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
@ -156,7 +186,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
return super.close(); return super.close();
} }
void initializeGroups(List<GroupPB> groups) { void initializeGroups(List<GroupPB> groupsData) {
for (var controller in groupControllers.values) { for (var controller in groupControllers.values) {
controller.dispose(); controller.dispose();
} }
@ -164,27 +194,27 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
boardController.clear(); boardController.clear();
// //
List<AppFlowyGroupData> columns = groups List<AppFlowyGroupData> groups = groupsData
.where((group) => fieldController.getField(group.fieldId) != null) .where((group) => fieldController.getField(group.fieldId) != null)
.map((group) { .map((group) {
return AppFlowyGroupData( return AppFlowyGroupData(
id: group.groupId, id: group.groupId,
name: group.desc, name: group.desc,
items: _buildRows(group), items: _buildGroupItems(group),
customData: BoardCustomData( customData: GroupData(
group: group, group: group,
fieldContext: fieldController.getField(group.fieldId)!, fieldContext: fieldController.getField(group.fieldId)!,
), ),
); );
}).toList(); }).toList();
boardController.addGroups(columns); boardController.addGroups(groups);
for (final group in groups) { for (final group in groupsData) {
final delegate = GroupControllerDelegateImpl( final delegate = GroupControllerDelegateImpl(
controller: boardController, controller: boardController,
fieldController: fieldController, fieldController: fieldController,
onNewColumnItem: (groupId, row, index) { onNewColumnItem: (groupId, row, index) {
add(BoardEvent.didCreateRow(groupId, row, index)); add(BoardEvent.didCreateRow(group, row, index));
}, },
); );
final controller = GroupController( final controller = GroupController(
@ -242,10 +272,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
} }
List<AppFlowyGroupItem> _buildRows(GroupPB group) { List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
final items = group.rows.map((row) { final items = group.rows.map((row) {
final fieldContext = fieldController.getField(group.fieldId); final fieldContext = fieldController.getField(group.fieldId);
return BoardColumnItem(row: row, fieldContext: fieldContext!); return GroupItem(
row: row,
fieldContext: fieldContext!,
);
}).toList(); }).toList();
return <AppFlowyGroupItem>[...items]; return <AppFlowyGroupItem>[...items];
@ -270,11 +303,15 @@ class BoardEvent with _$BoardEvent {
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow; const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.didCreateRow( const factory BoardEvent.didCreateRow(
String groupId, GroupPB group,
RowPB row, RowPB row,
int? index, int? index,
) = _DidCreateRow; ) = _DidCreateRow;
const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.startEditingRow(
GroupPB group,
RowPB row,
) = _StartEditRow;
const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveGridUpdate( const factory BoardEvent.didReceiveGridUpdate(
GridPB grid, GridPB grid,
@ -334,14 +371,17 @@ class GridFieldEquatable extends Equatable {
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields); UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
} }
class BoardColumnItem extends AppFlowyGroupItem { class GroupItem extends AppFlowyGroupItem {
final RowPB row; final RowPB row;
final GridFieldContext fieldContext; final GridFieldContext fieldContext;
BoardColumnItem({ GroupItem({
required this.row, required this.row,
required this.fieldContext, required this.fieldContext,
}); bool draggable = true,
}) {
super.draggable = draggable;
}
@override @override
String get id => row.id; String get id => row.id;
@ -367,10 +407,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
} }
if (index != null) { if (index != null) {
final item = BoardColumnItem(row: row, fieldContext: fieldContext); final item = GroupItem(
row: row,
fieldContext: fieldContext,
);
controller.insertGroupItem(group.groupId, index, item); controller.insertGroupItem(group.groupId, index, item);
} else { } else {
final item = BoardColumnItem(row: row, fieldContext: fieldContext); final item = GroupItem(
row: row,
fieldContext: fieldContext,
);
controller.addGroupItem(group.groupId, item); controller.addGroupItem(group.groupId, item);
} }
} }
@ -389,7 +435,10 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
} }
controller.updateGroupItem( controller.updateGroupItem(
group.groupId, group.groupId,
BoardColumnItem(row: row, fieldContext: fieldContext), GroupItem(
row: row,
fieldContext: fieldContext,
),
); );
} }
@ -400,7 +449,11 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
Log.warn("FieldContext should not be null"); Log.warn("FieldContext should not be null");
return; return;
} }
final item = BoardColumnItem(row: row, fieldContext: fieldContext); final item = GroupItem(
row: row,
fieldContext: fieldContext,
draggable: false,
);
if (index != null) { if (index != null) {
controller.insertGroupItem(group.groupId, index, item); controller.insertGroupItem(group.groupId, index, item);
@ -412,21 +465,21 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
} }
class BoardEditingRow { class BoardEditingRow {
String columnId; GroupPB group;
RowPB row; RowPB row;
int? index; int? index;
BoardEditingRow({ BoardEditingRow({
required this.columnId, required this.group,
required this.row, required this.row,
required this.index, required this.index,
}); });
} }
class BoardCustomData { class GroupData {
final GroupPB group; final GroupPB group;
final GridFieldContext fieldContext; final GridFieldContext fieldContext;
BoardCustomData({ GroupData({
required this.group, required this.group,
required this.fieldContext, required this.fieldContext,
}); });

View File

@ -87,13 +87,13 @@ class BoardDataController {
onUpdatedGroup.call(changeset.updateGroups); onUpdatedGroup.call(changeset.updateGroups);
} }
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
if (changeset.deletedGroups.isNotEmpty) { if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups); onDeletedGroup.call(changeset.deletedGroups);
} }
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
}, },
(e) => _onError?.call(e), (e) => _onError?.call(e),
); );

View File

@ -83,7 +83,7 @@ class _BoardContentState extends State<BoardContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<BoardBloc, BoardState>( return BlocListener<BoardBloc, BoardState>(
listener: (context, state) => _handleEditState(state, context), listener: (context, state) => _handleEditStateChanged(state, context),
child: BlocBuilder<BoardBloc, BoardState>( child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => previous.groupIds != current.groupIds, buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) { builder: (context, state) {
@ -128,21 +128,14 @@ class _BoardContentState extends State<BoardContent> {
); );
} }
void _handleEditState(BoardState state, BuildContext context) { void _handleEditStateChanged(BoardState state, BuildContext context) {
state.editingRow.fold( state.editingRow.fold(
() => null, () => null,
(editingRow) { (editingRow) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (editingRow.index != null) { if (editingRow.index != null) {
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
} else { } else {
scrollManager.scrollToBottom(editingRow.columnId, (boardContext) { scrollManager.scrollToBottom(editingRow.group.groupId);
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
});
} }
}); });
}, },
@ -156,14 +149,14 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildHeader( Widget _buildHeader(
BuildContext context, BuildContext context,
AppFlowyGroupData columnData, AppFlowyGroupData groupData,
) { ) {
final boardCustomData = columnData.customData as BoardCustomData; final boardCustomData = groupData.customData as GroupData;
return AppFlowyGroupHeader( return AppFlowyGroupHeader(
title: Flexible( title: Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
child: FlowyText.medium( child: FlowyText.medium(
columnData.headerData.groupName, groupData.headerData.groupName,
fontSize: 14, fontSize: 14,
overflow: TextOverflow.clip, overflow: TextOverflow.clip,
color: context.read<AppTheme>().textColor, color: context.read<AppTheme>().textColor,
@ -180,7 +173,7 @@ class _BoardContentState extends State<BoardContent> {
), ),
onAddButtonClick: () { onAddButtonClick: () {
context.read<BoardBloc>().add( context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(columnData.id), BoardEvent.createHeaderRow(groupData.id),
); );
}, },
height: 50, height: 50,
@ -218,15 +211,16 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildCard( Widget _buildCard(
BuildContext context, BuildContext context,
AppFlowyGroupData group, AppFlowyGroupData afGroupData,
AppFlowyGroupItem columnItem, AppFlowyGroupItem afGroupItem,
) { ) {
final boardColumnItem = columnItem as BoardColumnItem; final groupItem = afGroupItem as GroupItem;
final rowPB = boardColumnItem.row; final groupData = afGroupData.customData as GroupData;
final rowPB = groupItem.row;
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId); final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null. /// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(columnItem)); if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
final fieldController = context.read<BoardBloc>().fieldController; final fieldController = context.read<BoardBloc>().fieldController;
final gridId = context.read<BoardBloc>().gridId; final gridId = context.read<BoardBloc>().gridId;
@ -241,19 +235,19 @@ class _BoardContentState extends State<BoardContent> {
context.read<BoardBloc>().state.editingRow.fold( context.read<BoardBloc>().state.editingRow.fold(
() => null, () => null,
(editingRow) { (editingRow) {
isEditing = editingRow.row.id == columnItem.row.id; isEditing = editingRow.row.id == groupItem.row.id;
}, },
); );
final groupItemId = columnItem.id + group.id; final groupItemId = groupItem.row.id + groupData.group.groupId;
return AppFlowyGroupCard( return AppFlowyGroupCard(
key: ValueKey(groupItemId), key: ValueKey(groupItemId),
margin: config.cardPadding, margin: config.cardPadding,
decoration: _makeBoxDecoration(context), decoration: _makeBoxDecoration(context),
child: BoardCard( child: BoardCard(
gridId: gridId, gridId: gridId,
groupId: group.id, groupId: groupData.group.groupId,
fieldId: boardColumnItem.fieldContext.id, fieldId: groupItem.fieldContext.id,
isEditing: isEditing, isEditing: isEditing,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
dataController: cardController, dataController: cardController,
@ -264,6 +258,19 @@ class _BoardContentState extends State<BoardContent> {
rowCache, rowCache,
context, context,
), ),
onStartEditing: () {
context.read<BoardBloc>().add(
BoardEvent.startEditingRow(
groupData.group,
groupItem.row,
),
);
},
onEndEditing: () {
context
.read<BoardBloc>()
.add(BoardEvent.endEditingRow(groupItem.row.id));
},
), ),
); );
} }
@ -345,7 +352,7 @@ extension HexColor on Color {
} }
} }
Widget? _buildHeaderIcon(BoardCustomData customData) { Widget? _buildHeaderIcon(GroupData customData) {
Widget? widget; Widget? widget;
switch (customData.fieldType) { switch (customData.fieldType) {
case FieldType.Checkbox: case FieldType.Checkbox:

View File

@ -76,6 +76,10 @@ class EditableRowNotifier {
} }
abstract class EditableCell { abstract class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCellNotifier? get editableNotifier; EditableCellNotifier? get editableNotifier;
} }

View File

@ -42,6 +42,9 @@ class _BoardTextCellState extends State<BoardTextCell> {
focusNode.requestFocus(); focusNode.requestFocus();
} }
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() { focusNode.addListener(() {
if (!focusNode.hasFocus) { if (!focusNode.hasFocus) {
focusWhenInit = false; focusWhenInit = false;
@ -131,7 +134,11 @@ class _BoardTextCellState extends State<BoardTextCell> {
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: BoardSizes.cardCellVPadding, vertical: BoardSizes.cardCellVPadding,
), ),
child: FlowyText.medium(state.content, fontSize: 14), child: FlowyText.medium(
state.content,
fontSize: 14,
maxLines: null, // Enable multiple lines
),
); );
} }

View File

@ -21,6 +21,8 @@ class BoardCard extends StatefulWidget {
final CardDataController dataController; final CardDataController dataController;
final BoardCellBuilder cellBuilder; final BoardCellBuilder cellBuilder;
final void Function(BuildContext) openCard; final void Function(BuildContext) openCard;
final VoidCallback onStartEditing;
final VoidCallback onEndEditing;
const BoardCard({ const BoardCard({
required this.gridId, required this.gridId,
@ -30,6 +32,8 @@ class BoardCard extends StatefulWidget {
required this.dataController, required this.dataController,
required this.cellBuilder, required this.cellBuilder,
required this.openCard, required this.openCard,
required this.onStartEditing,
required this.onEndEditing,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -56,6 +60,12 @@ class _BoardCardState extends State<BoardCard> {
rowNotifier.isEditing.addListener(() { rowNotifier.isEditing.addListener(() {
if (!mounted) return; if (!mounted) return;
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value)); _cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) {
widget.onStartEditing();
} else {
widget.onEndEditing();
}
}); });
popoverController = PopoverController(); popoverController = PopoverController();

View File

@ -15,6 +15,7 @@ import 'package:clipboard/clipboard.dart';
import 'package:dartz/dartz.dart' as dartz; import 'package:dartz/dartz.dart' as dartz;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
@ -112,7 +113,6 @@ class DocumentShareButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double buttonWidth = 60;
return BlocProvider( return BlocProvider(
create: (context) => getIt<DocShareBloc>(param1: view), create: (context) => getIt<DocShareBloc>(param1: view),
child: BlocListener<DocShareBloc, DocShareState>( child: BlocListener<DocShareBloc, DocShareState>(
@ -130,6 +130,7 @@ class DocumentShareButton extends StatelessWidget {
}, },
child: BlocBuilder<DocShareBloc, DocShareState>( child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) { builder: (context, state) {
final theme = context.watch<AppTheme>();
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSetting>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSetting, Locale>( child: Selector<AppearanceSetting, Locale>(
@ -137,16 +138,15 @@ class DocumentShareButton extends StatelessWidget {
builder: (ctx, _, child) => ConstrainedBox( builder: (ctx, _, child) => ConstrainedBox(
constraints: const BoxConstraints.expand( constraints: const BoxConstraints.expand(
height: 30, height: 30,
// minWidth: buttonWidth,
width: 100, width: 100,
), ),
child: RoundedTextButton( child: RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(), title: LocaleKeys.shareAction_buttonText.tr(),
fontSize: 12, fontSize: 12,
borderRadius: Corners.s6Border, borderRadius: Corners.s6Border,
color: Colors.lightBlue, color: theme.main1,
onPressed: () => _showActionList( onPressed: () =>
context, Offset(-(buttonWidth / 2), 10)), _showActionList(context, const Offset(0, 10)),
), ),
), ),
), ),
@ -193,7 +193,7 @@ class DocumentShareButton extends StatelessWidget {
}); });
actionList.show( actionList.show(
context, context,
anchorDirection: AnchorDirection.bottomWithCenterAligned, anchorDirection: AnchorDirection.bottomWithRightAligned,
anchorOffset: offset, anchorOffset: offset,
); );
} }

View File

@ -159,6 +159,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
focusedDay: state.focusedDay, focusedDay: state.focusedDay,
rowHeight: 40, rowHeight: 40,
calendarFormat: state.format, calendarFormat: state.format,
daysOfWeekHeight: 40,
headerStyle: HeaderStyle( headerStyle: HeaderStyle(
formatButtonVisible: false, formatButtonVisible: false,
titleCentered: true, titleCentered: true,
@ -168,6 +169,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
rightChevronPadding: EdgeInsets.zero, rightChevronPadding: EdgeInsets.zero,
rightChevronMargin: EdgeInsets.zero, rightChevronMargin: EdgeInsets.zero,
rightChevronIcon: svgWidget("home/arrow_right"), rightChevronIcon: svgWidget("home/arrow_right"),
headerMargin: const EdgeInsets.only(bottom: 8.0),
), ),
daysOfWeekStyle: DaysOfWeekStyle( daysOfWeekStyle: DaysOfWeekStyle(
dowTextFormatter: (date, locale) => dowTextFormatter: (date, locale) =>
@ -182,13 +184,31 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
), ),
), ),
calendarStyle: CalendarStyle( calendarStyle: CalendarStyle(
cellMargin: const EdgeInsets.all(3),
defaultDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
selectedDecoration: BoxDecoration( selectedDecoration: BoxDecoration(
color: theme.main1, color: theme.main1,
shape: BoxShape.circle, shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
), ),
todayDecoration: BoxDecoration( todayDecoration: BoxDecoration(
color: theme.shader4, color: theme.shader4,
shape: BoxShape.circle, shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
weekendDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
outsideDecoration: BoxDecoration(
color: theme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
), ),
selectedTextStyle: TextStyle( selectedTextStyle: TextStyle(
color: theme.surface, color: theme.surface,

View File

@ -142,11 +142,12 @@ class _TextField extends StatelessWidget {
value: (option) => option); value: (option) => option);
return SizedBox( return SizedBox(
height: 42, height: 62,
child: SelectOptionTextField( child: SelectOptionTextField(
options: state.options, options: state.options,
selectedOptionMap: optionMap, selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7, distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: _tagController, tagController: _tagController,
onClick: () => popoverMutex.close(), onClick: () => popoverMutex.close(),
newText: (text) { newText: (text) {

View File

@ -6,6 +6,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:textfield_tags/textfield_tags.dart'; import 'package:textfield_tags/textfield_tags.dart';
@ -20,6 +21,7 @@ class SelectOptionTextField extends StatefulWidget {
final Function(String) onSubmitted; final Function(String) onSubmitted;
final Function(String) newText; final Function(String) newText;
final VoidCallback? onClick; final VoidCallback? onClick;
final int? maxLength;
const SelectOptionTextField({ const SelectOptionTextField({
required this.options, required this.options,
@ -29,6 +31,7 @@ class SelectOptionTextField extends StatefulWidget {
required this.onSubmitted, required this.onSubmitted,
required this.newText, required this.newText,
this.onClick, this.onClick,
this.maxLength,
TextEditingController? textController, TextEditingController? textController,
FocusNode? focusNode, FocusNode? focusNode,
Key? key, Key? key,
@ -93,6 +96,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
} }
}, },
maxLines: 1, maxLines: 1,
maxLength: widget.maxLength,
maxLengthEnforcement:
MaxLengthEnforcement.truncateAfterCompositionEnds,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: InputDecoration( decoration: InputDecoration(
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(

View File

@ -9,6 +9,7 @@ class InputTextField extends StatefulWidget {
final void Function() onCanceled; final void Function() onCanceled;
final bool autoClearWhenDone; final bool autoClearWhenDone;
final String text; final String text;
final int? maxLength;
const InputTextField({ const InputTextField({
required this.text, required this.text,
@ -16,6 +17,7 @@ class InputTextField extends StatefulWidget {
required this.onCanceled, required this.onCanceled,
this.onChanged, this.onChanged,
this.autoClearWhenDone = false, this.autoClearWhenDone = false,
this.maxLength,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -41,11 +43,14 @@ class _InputTextFieldState extends State<InputTextField> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final height = widget.maxLength == null ? 36.0 : 56.0;
return RoundedInputField( return RoundedInputField(
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
autoFocus: true, autoFocus: true,
height: 36, height: height,
maxLength: widget.maxLength,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
normalBorderColor: theme.shader4, normalBorderColor: theme.shader4,
focusBorderColor: theme.main1, focusBorderColor: theme.main1,

View File

@ -256,6 +256,7 @@ class _CreateOptionTextField extends StatelessWidget {
final text = state.newOptionName.foldRight("", (a, previous) => a); final text = state.newOptionName.foldRight("", (a, previous) => a);
return InputTextField( return InputTextField(
autoClearWhenDone: true, autoClearWhenDone: true,
maxLength: 30,
text: text, text: text,
onCanceled: () { onCanceled: () {
context context

View File

@ -106,6 +106,7 @@ class _OptionNameTextField extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InputTextField( return InputTextField(
text: name, text: name,
maxLength: 30,
onCanceled: () {}, onCanceled: () {},
onDone: (optionName) { onDone: (optionName) {
if (name != optionName) { if (name != optionName) {

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/user/application/user_listener.dart'; import 'package:app_flowy/user/application/user_listener.dart';
import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart'; import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@ -50,13 +51,24 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
unauthorized: (_Unauthorized value) { unauthorized: (_Unauthorized value) {
emit(state.copyWith(unauthorized: true)); emit(state.copyWith(unauthorized: true));
}, },
collapseMenu: (e) { collapseMenu: (_CollapseMenu e) {
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
}, },
editPanelResized: (e) { editPanelResizeStart: (_EditPanelResizeStart e) {
final newOffset = emit(state.copyWith(
(state.resizeOffset + e.offset).clamp(-50, 200).toDouble(); resizeType: MenuResizeType.drag,
emit(state.copyWith(resizeOffset: newOffset)); resizeStart: state.resizeOffset,
));
},
editPanelResized: (_EditPanelResized e) {
final newPosition =
(e.offset + state.resizeStart).clamp(-50, 200).toDouble();
if (state.resizeOffset != newPosition) {
emit(state.copyWith(resizeOffset: newPosition));
}
},
editPanelResizeEnd: (_EditPanelResizeEnd e) {
emit(state.copyWith(resizeType: MenuResizeType.slide));
}, },
); );
}, },
@ -78,6 +90,22 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
} }
} }
enum MenuResizeType {
slide,
drag,
}
extension MenuResizeTypeExtension on MenuResizeType {
Duration duration() {
switch (this) {
case MenuResizeType.drag:
return 30.milliseconds;
case MenuResizeType.slide:
return 350.milliseconds;
}
}
}
@freezed @freezed
class HomeEvent with _$HomeEvent { class HomeEvent with _$HomeEvent {
const factory HomeEvent.initial() = _Initial; const factory HomeEvent.initial() = _Initial;
@ -91,6 +119,8 @@ class HomeEvent with _$HomeEvent {
const factory HomeEvent.unauthorized(String msg) = _Unauthorized; const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
const factory HomeEvent.collapseMenu() = _CollapseMenu; const factory HomeEvent.collapseMenu() = _CollapseMenu;
const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized; const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized;
const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart;
const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
} }
@freezed @freezed
@ -103,6 +133,8 @@ class HomeState with _$HomeState {
required bool unauthorized, required bool unauthorized,
required bool isMenuCollapsed, required bool isMenuCollapsed,
required double resizeOffset, required double resizeOffset,
required double resizeStart,
required MenuResizeType resizeType,
}) = _HomeState; }) = _HomeState;
factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) => factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) =>
@ -114,5 +146,7 @@ class HomeState with _$HomeState {
unauthorized: false, unauthorized: false,
isMenuCollapsed: false, isMenuCollapsed: false,
resizeOffset: 0, resizeOffset: 0,
resizeStart: 0,
resizeType: MenuResizeType.slide,
); );
} }

View File

@ -2,7 +2,6 @@ import 'dart:io' show Platform;
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// ignore: import_of_legacy_library_into_null_safe // ignore: import_of_legacy_library_into_null_safe
import 'package:sized_context/sized_context.dart'; import 'package:sized_context/sized_context.dart';
@ -44,7 +43,7 @@ class HomeLayout {
homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0; homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;
menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0; menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;
animDuration = .35.seconds; animDuration = homeBlocState.resizeType.duration();
editPanelWidth = HomeSizes.editPanelWidth; editPanelWidth = HomeSizes.editPanelWidth;
homePageROffset = showEditPanel ? editPanelWidth : 0; homePageROffset = showEditPanel ? editPanelWidth : 0;

View File

@ -176,11 +176,18 @@ class _HomeScreenState extends State<HomeScreen> {
cursor: SystemMouseCursors.resizeLeftRight, cursor: SystemMouseCursors.resizeLeftRight,
child: GestureDetector( child: GestureDetector(
dragStartBehavior: DragStartBehavior.down, dragStartBehavior: DragStartBehavior.down,
onPanUpdate: ((details) { onHorizontalDragStart: (details) => context
context .read<HomeBloc>()
.read<HomeBloc>() .add(const HomeEvent.editPanelResizeStart()),
.add(HomeEvent.editPanelResized(details.delta.dx)); onHorizontalDragUpdate: (details) => context
}), .read<HomeBloc>()
.add(HomeEvent.editPanelResized(details.localPosition.dx)),
onHorizontalDragEnd: (details) => context
.read<HomeBloc>()
.add(const HomeEvent.editPanelResizeEnd()),
onHorizontalDragCancel: () => context
.read<HomeBloc>()
.add(const HomeEvent.editPanelResizeEnd()),
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: SizedBox( child: SizedBox(
width: 10, width: 10,
@ -208,7 +215,6 @@ class _HomeScreenState extends State<HomeScreen> {
top: 0, top: 0,
animate: true) animate: true)
.animate(layout.animDuration, Curves.easeOut), .animate(layout.animDuration, Curves.easeOut),
homeMenuResizer.positioned(left: layout.homePageLOffset - 5),
bubble bubble
.positioned( .positioned(
right: 20, right: 20,
@ -236,6 +242,9 @@ class _HomeScreenState extends State<HomeScreen> {
bottom: 0, bottom: 0,
animate: true) animate: true)
.animate(layout.animDuration, Curves.easeOut), .animate(layout.animDuration, Curves.easeOut),
homeMenuResizer
.positioned(left: layout.homePageLOffset - 5)
.animate(layout.animDuration, Curves.easeOut),
], ],
); );
} }

View File

@ -97,10 +97,8 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
child: SizedBox( child: SizedBox(
height: itemHeight, height: itemHeight,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (icon != null) icon, if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
HSpace(ActionListSizes.itemHPadding),
FlowyText.medium(action.name, fontSize: 12), FlowyText.medium(action.name, fontSize: 12),
], ],
), ),

View File

@ -1,8 +1,14 @@
# 0.0.9
* Enable slide to select text in card
* Fix some bugs
# 0.0.8 # 0.0.8
* Enable drag and drop group * Enable drag and drop group
# 0.0.7 # 0.0.7
* Rename some classes * Rename some classes
* Add documentation * Add documentation
# 0.0.6 # 0.0.6
* Support scroll to bottom * Support scroll to bottom
* Fix some bugs * Fix some bugs

View File

@ -78,7 +78,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
height: 50, height: 50,
margin: config.groupItemPadding, margin: config.groupItemPadding,
onAddButtonClick: () { onAddButtonClick: () {
boardController.scrollToBottom(columnData.id, (p0) {}); boardController.scrollToBottom(columnData.id);
}, },
); );
}, },

View File

@ -32,4 +32,8 @@ class Log {
'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message'); 'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
} }
} }
static void error(String? message) {
debugPrint('AppFlowyBoard: ❌[Error] - ${DateTime.now().second}=> $message');
}
} }

View File

@ -11,10 +11,11 @@ import 'reorder_phantom/phantom_controller.dart';
import '../rendering/board_overlay.dart'; import '../rendering/board_overlay.dart';
class AppFlowyBoardScrollController { class AppFlowyBoardScrollController {
AppFlowyBoardState? _groupState; AppFlowyBoardState? _boardState;
void scrollToBottom(String groupId, void Function(BuildContext)? completed) { void scrollToBottom(String groupId,
_groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed); {void Function(BuildContext)? completed}) {
_boardState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
} }
} }
@ -39,9 +40,6 @@ class AppFlowyBoardConfig {
} }
class AppFlowyBoard extends StatelessWidget { class AppFlowyBoard extends StatelessWidget {
/// The direction to use as the main axis.
final Axis direction = Axis.vertical;
/// The widget that will be rendered as the background of the board. /// The widget that will be rendered as the background of the board.
final Widget? background; final Widget? background;
@ -94,11 +92,7 @@ class AppFlowyBoard extends StatelessWidget {
/// ///
final AppFlowyBoardScrollController? boardScrollController; final AppFlowyBoardScrollController? boardScrollController;
final AppFlowyBoardState _groupState = AppFlowyBoardState(); const AppFlowyBoard({
late final BoardPhantomController _phantomController;
AppFlowyBoard({
required this.controller, required this.controller,
required this.cardBuilder, required this.cardBuilder,
this.background, this.background,
@ -109,12 +103,7 @@ class AppFlowyBoard extends StatelessWidget {
this.groupConstraints = const BoxConstraints(maxWidth: 200), this.groupConstraints = const BoxConstraints(maxWidth: 200),
this.config = const AppFlowyBoardConfig(), this.config = const AppFlowyBoardConfig(),
Key? key, Key? key,
}) : super(key: key) { }) : super(key: key);
_phantomController = BoardPhantomController(
delegate: controller,
groupsState: _groupState,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -122,8 +111,14 @@ class AppFlowyBoard extends StatelessWidget {
value: controller, value: controller,
child: Consumer<AppFlowyBoardController>( child: Consumer<AppFlowyBoardController>(
builder: (context, notifier, child) { builder: (context, notifier, child) {
final boardState = AppFlowyBoardState();
BoardPhantomController phantomController = BoardPhantomController(
delegate: controller,
groupsState: boardState,
);
if (boardScrollController != null) { if (boardScrollController != null) {
boardScrollController!._groupState = _groupState; boardScrollController!._boardState = boardState;
} }
return _AppFlowyBoardContent( return _AppFlowyBoardContent(
@ -131,14 +126,14 @@ class AppFlowyBoard extends StatelessWidget {
dataController: controller, dataController: controller,
scrollController: scrollController, scrollController: scrollController,
scrollManager: boardScrollController, scrollManager: boardScrollController,
groupState: _groupState, boardState: boardState,
background: background, background: background,
delegate: _phantomController, delegate: phantomController,
groupConstraints: groupConstraints, groupConstraints: groupConstraints,
cardBuilder: cardBuilder, cardBuilder: cardBuilder,
footerBuilder: footerBuilder, footerBuilder: footerBuilder,
headerBuilder: headerBuilder, headerBuilder: headerBuilder,
phantomController: _phantomController, phantomController: phantomController,
onReorder: controller.moveGroup, onReorder: controller.moveGroup,
); );
}, },
@ -156,7 +151,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
final ReorderFlexConfig reorderFlexConfig; final ReorderFlexConfig reorderFlexConfig;
final BoxConstraints groupConstraints; final BoxConstraints groupConstraints;
final AppFlowyBoardScrollController? scrollManager; final AppFlowyBoardScrollController? scrollManager;
final AppFlowyBoardState groupState; final AppFlowyBoardState boardState;
final AppFlowyBoardCardBuilder cardBuilder; final AppFlowyBoardCardBuilder cardBuilder;
final AppFlowyBoardHeaderBuilder? headerBuilder; final AppFlowyBoardHeaderBuilder? headerBuilder;
final AppFlowyBoardFooterBuilder? footerBuilder; final AppFlowyBoardFooterBuilder? footerBuilder;
@ -169,7 +164,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
required this.delegate, required this.delegate,
required this.dataController, required this.dataController,
required this.scrollManager, required this.scrollManager,
required this.groupState, required this.boardState,
this.scrollController, this.scrollController,
this.background, this.background,
required this.groupConstraints, required this.groupConstraints,
@ -178,7 +173,10 @@ class _AppFlowyBoardContent extends StatefulWidget {
this.headerBuilder, this.headerBuilder,
required this.phantomController, required this.phantomController,
Key? key, Key? key,
}) : reorderFlexConfig = const ReorderFlexConfig(), }) : reorderFlexConfig = const ReorderFlexConfig(
direction: Axis.horizontal,
dragDirection: Axis.horizontal,
),
super(key: key); super(key: key);
@override @override
@ -198,7 +196,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
reorderFlexId: widget.dataController.identifier, reorderFlexId: widget.dataController.identifier,
acceptedReorderFlexId: widget.dataController.groupIds, acceptedReorderFlexId: widget.dataController.groupIds,
delegate: widget.delegate, delegate: widget.delegate,
columnsState: widget.groupState, columnsState: widget.boardState,
); );
final reorderFlex = ReorderFlex( final reorderFlex = ReorderFlex(
@ -206,9 +204,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
scrollController: widget.scrollController, scrollController: widget.scrollController,
onReorder: widget.onReorder, onReorder: widget.onReorder,
dataSource: widget.dataController, dataSource: widget.dataController,
direction: Axis.horizontal,
interceptor: interceptor, interceptor: interceptor,
reorderable: true,
children: _buildColumns(), children: _buildColumns(),
); );
@ -254,7 +250,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
); );
final reorderFlexAction = ReorderFlexActionImpl(); final reorderFlexAction = ReorderFlexActionImpl();
widget.groupState.reorderFlexActionMap[columnData.id] = widget.boardState.reorderFlexActionMap[columnData.id] =
reorderFlexAction; reorderFlexAction;
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
@ -275,8 +271,8 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
onReorder: widget.dataController.moveGroupItem, onReorder: widget.dataController.moveGroupItem,
cornerRadius: widget.config.cornerRadius, cornerRadius: widget.config.cornerRadius,
backgroundColor: widget.config.groupBackgroundColor, backgroundColor: widget.config.groupBackgroundColor,
dragStateStorage: widget.groupState, dragStateStorage: widget.boardState,
dragTargetKeys: widget.groupState, dragTargetKeys: widget.boardState,
reorderFlexAction: reorderFlexAction, reorderFlexAction: reorderFlexAction,
); );

View File

@ -138,7 +138,11 @@ class AppFlowyBoardController extends ChangeNotifier
/// groups or get ready to reinitialize the [AppFlowyBoard]. /// groups or get ready to reinitialize the [AppFlowyBoard].
void clear() { void clear() {
_groupDatas.clear(); _groupDatas.clear();
for (final group in _groupControllers.values) {
group.dispose();
}
_groupControllers.clear(); _groupControllers.clear();
notifyListeners(); notifyListeners();
} }
@ -202,6 +206,14 @@ class AppFlowyBoardController extends ChangeNotifier
getGroupController(groupId)?.replaceOrInsertItem(item); getGroupController(groupId)?.replaceOrInsertItem(item);
} }
void enableGroupDragging(bool isEnable) {
for (var groupController in _groupControllers.values) {
groupController.enableDragging(isEnable);
}
notifyListeners();
}
/// Moves the item at [fromGroupIndex] in group with id [fromGroupId] to /// Moves the item at [fromGroupIndex] in group with id [fromGroupId] to
/// group with id [toGroupId] at [toGroupIndex] /// group with id [toGroupId] at [toGroupIndex]
@override @override
@ -215,6 +227,8 @@ class AppFlowyBoardController extends ChangeNotifier
final fromGroupController = getGroupController(fromGroupId)!; final fromGroupController = getGroupController(fromGroupId)!;
final toGroupController = getGroupController(toGroupId)!; final toGroupController = getGroupController(toGroupId)!;
final fromGroupItem = fromGroupController.removeAt(fromGroupIndex); final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
if (fromGroupItem == null) return;
if (toGroupController.items.length > toGroupIndex) { if (toGroupController.items.length > toGroupIndex) {
assert(toGroupController.items[toGroupIndex] is PhantomGroupItem); assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
@ -275,7 +289,9 @@ class AppFlowyBoardController extends ChangeNotifier
Log.trace( Log.trace(
'[$BoardPhantomController] update $groupId:$index to $groupId:$newIndex'); '[$BoardPhantomController] update $groupId:$index to $groupId:$newIndex');
final item = groupController.removeAt(index, notify: false); final item = groupController.removeAt(index, notify: false);
groupController.insert(newIndex, item, notify: false); if (item != null) {
groupController.insert(newIndex, item, notify: false);
}
} }
} }
} }

View File

@ -156,9 +156,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
widget.onDragStarted?.call(index); widget.onDragStarted?.call(index);
}, },
onReorder: ((fromIndex, toIndex) { onReorder: ((fromIndex, toIndex) {
if (widget.phantomController.isFromGroup(widget.groupId)) { if (widget.phantomController.shouldReorder(widget.groupId)) {
widget.onReorder(widget.groupId, fromIndex, toIndex); widget.onReorder(widget.groupId, fromIndex, toIndex);
widget.phantomController.transformIndex(fromIndex, toIndex); widget.phantomController.updateIndex(fromIndex, toIndex);
} }
}), }),
onDragEnded: () { onDragEnded: () {

View File

@ -5,6 +5,8 @@ import 'package:appflowy_board/src/widgets/reorder_flex/reorder_flex.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
typedef IsDraggable = bool;
/// A item represents the generic data model of each group card. /// A item represents the generic data model of each group card.
/// ///
/// Each item displayed in the group required to implement this class. /// Each item displayed in the group required to implement this class.
@ -50,8 +52,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
/// * [notify] the default value of [notify] is true, it will notify the /// * [notify] the default value of [notify] is true, it will notify the
/// listener. Set to false if you do not want to notify the listeners. /// listener. Set to false if you do not want to notify the listeners.
/// ///
AppFlowyGroupItem removeAt(int index, {bool notify = true}) { AppFlowyGroupItem? removeAt(int index, {bool notify = true}) {
assert(index >= 0); if (groupData._items.length <= index) {
Log.error(
'Fatal error, index is out of bounds. Index: $index, len: ${groupData._items.length}');
return null;
}
if (index < 0) {
Log.error('Invalid index:$index');
return null;
}
Log.debug('[$AppFlowyGroupController] $groupData remove item at $index'); Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
final item = groupData._items.removeAt(index); final item = groupData._items.removeAt(index);
@ -71,12 +82,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
/// Move the item from [fromIndex] to [toIndex]. It will do nothing if the /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
/// [fromIndex] equal to the [toIndex]. /// [fromIndex] equal to the [toIndex].
bool move(int fromIndex, int toIndex) { bool move(int fromIndex, int toIndex) {
assert(fromIndex >= 0);
assert(toIndex >= 0); assert(toIndex >= 0);
if (groupData._items.length < fromIndex) {
Log.error(
'Out of bounds error. index: $fromIndex should not greater than ${groupData._items.length}');
return false;
}
if (fromIndex == toIndex) { if (fromIndex == toIndex) {
return false; return false;
} }
Log.debug( Log.debug(
'[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex'); '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
final item = groupData._items.removeAt(fromIndex); final item = groupData._items.removeAt(fromIndex);
@ -124,7 +140,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
Log.debug('[$AppFlowyGroupController] $groupData add $newItem'); Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
} else { } else {
if (index >= groupData._items.length) { if (index >= groupData._items.length) {
Log.warn( Log.error(
'[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}'); '[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
return; return;
} }
@ -155,6 +171,15 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
-1; -1;
} }
void enableDragging(bool isEnable) {
groupData.draggable = isEnable;
for (var item in groupData._items) {
item.draggable = isEnable;
}
_notify();
}
void _notify() { void _notify() {
notifyListeners(); notifyListeners();
} }

View File

@ -16,13 +16,13 @@ class FlexDragTargetData extends DragTargetData {
@override @override
final int draggingIndex; final int draggingIndex;
final DraggingState _state; final DraggingState _draggingState;
Widget? get draggingWidget => _state.draggingWidget; Widget? get draggingWidget => _draggingState.draggingWidget;
Size? get feedbackSize => _state.feedbackSize; Size? get feedbackSize => _draggingState.feedbackSize;
bool get isDragging => _state.isDragging(); bool get isDragging => _draggingState.isDragging();
final String dragTargetId; final String dragTargetId;
@ -40,8 +40,8 @@ class FlexDragTargetData extends DragTargetData {
required this.reorderFlexId, required this.reorderFlexId,
required this.reorderFlexItem, required this.reorderFlexItem,
required this.dragTargetIndexKey, required this.dragTargetIndexKey,
required DraggingState state, required DraggingState draggingState,
}) : _state = state; }) : _draggingState = draggingState;
@override @override
String toString() { String toString() {

View File

@ -1,3 +1,4 @@
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_board/src/utils/log.dart'; import 'package:appflowy_board/src/utils/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -78,10 +79,12 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
final bool useMoveAnimation; final bool useMoveAnimation;
final bool draggable; final IsDraggable draggable;
final double draggingOpacity; final double draggingOpacity;
final Axis? dragDirection;
const ReorderDragTarget({ const ReorderDragTarget({
Key? key, Key? key,
required this.child, required this.child,
@ -99,6 +102,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
this.onLeave, this.onLeave,
this.draggableTargetBuilder, this.draggableTargetBuilder,
this.draggingOpacity = 0.3, this.draggingOpacity = 0.3,
this.dragDirection,
}) : super(key: key); }) : super(key: key);
@override @override
@ -115,8 +119,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
Widget dragTarget = DragTarget<T>( Widget dragTarget = DragTarget<T>(
builder: _buildDraggableWidget, builder: _buildDraggableWidget,
onWillAccept: (dragTargetData) { onWillAccept: (dragTargetData) {
assert(dragTargetData != null); if (dragTargetData == null) {
if (dragTargetData == null) return false; return false;
}
return widget.onWillAccept(dragTargetData); return widget.onWillAccept(dragTargetData);
}, },
onAccept: widget.onAccept, onAccept: widget.onAccept,
@ -140,9 +146,6 @@ class _ReorderDragTargetState<T extends DragTargetData>
List<T?> acceptedCandidates, List<T?> acceptedCandidates,
List<dynamic> rejectedCandidates, List<dynamic> rejectedCandidates,
) { ) {
if (!widget.draggable) {
return widget.child;
}
Widget feedbackBuilder = Builder(builder: (BuildContext context) { Widget feedbackBuilder = Builder(builder: (BuildContext context) {
BoxConstraints contentSizeConstraints = BoxConstraints contentSizeConstraints =
BoxConstraints.loose(_draggingFeedbackSize!); BoxConstraints.loose(_draggingFeedbackSize!);
@ -163,7 +166,8 @@ class _ReorderDragTargetState<T extends DragTargetData>
widget.deleteAnimationController, widget.deleteAnimationController,
) ?? ) ??
Draggable<DragTargetData>( Draggable<DragTargetData>(
maxSimultaneousDrags: 1, axis: widget.dragDirection,
maxSimultaneousDrags: widget.draggable ? 1 : 0,
data: widget.dragTargetData, data: widget.dragTargetData,
ignoringFeedbackSemantics: false, ignoringFeedbackSemantics: false,
feedback: feedbackBuilder, feedback: feedbackBuilder,

View File

@ -1,6 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:math'; import 'dart:math';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import '../../utils/log.dart'; import '../../utils/log.dart';
@ -29,6 +30,8 @@ abstract class ReoderFlexDataSource {
abstract class ReoderFlexItem { abstract class ReoderFlexItem {
/// [id] is used to identify the item. It must be unique. /// [id] is used to identify the item. It must be unique.
String get id; String get id;
IsDraggable draggable = true;
} }
/// Cache each dragTarget's key. /// Cache each dragTarget's key.
@ -73,8 +76,15 @@ class ReorderFlexConfig {
final bool useMovePlaceholder; final bool useMovePlaceholder;
/// [direction] How to place the children, default is Axis.vertical
final Axis direction;
final Axis? dragDirection;
const ReorderFlexConfig({ const ReorderFlexConfig({
this.useMoveAnimation = true, this.useMoveAnimation = true,
this.direction = Axis.vertical,
this.dragDirection,
}) : useMovePlaceholder = !useMoveAnimation; }) : useMovePlaceholder = !useMoveAnimation;
} }
@ -82,8 +92,6 @@ class ReorderFlex extends StatefulWidget {
final ReorderFlexConfig config; final ReorderFlexConfig config;
final List<Widget> children; final List<Widget> children;
/// [direction] How to place the children, default is Axis.vertical
final Axis direction;
final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start; final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
final ScrollController? scrollController; final ScrollController? scrollController;
@ -108,8 +116,6 @@ class ReorderFlex extends StatefulWidget {
final ReorderFlexAction? reorderFlexAction; final ReorderFlexAction? reorderFlexAction;
final bool reorderable;
ReorderFlex({ ReorderFlex({
Key? key, Key? key,
this.scrollController, this.scrollController,
@ -117,14 +123,12 @@ class ReorderFlex extends StatefulWidget {
required this.children, required this.children,
required this.config, required this.config,
required this.onReorder, required this.onReorder,
this.reorderable = true,
this.dragStateStorage, this.dragStateStorage,
this.dragTargetKeys, this.dragTargetKeys,
this.onDragStarted, this.onDragStarted,
this.onDragEnded, this.onDragEnded,
this.interceptor, this.interceptor,
this.reorderFlexAction, this.reorderFlexAction,
this.direction = Axis.vertical,
}) : assert(children.every((Widget w) => w.key != null), }) : assert(children.every((Widget w) => w.key != null),
'All child must have a key.'), 'All child must have a key.'),
super(key: key); super(key: key);
@ -146,8 +150,8 @@ class ReorderFlexState extends State<ReorderFlex>
/// Whether or not we are currently scrolling this view to show a widget. /// Whether or not we are currently scrolling this view to show a widget.
bool _scrolling = false; bool _scrolling = false;
/// [dragState] records the dragging state including dragStartIndex, and phantomIndex, etc. /// [draggingState] records the dragging state including dragStartIndex, and phantomIndex, etc.
late DraggingState dragState; late DraggingState draggingState;
/// [_animation] controls the dragging animations /// [_animation] controls the dragging animations
late DragTargetAnimation _animation; late DragTargetAnimation _animation;
@ -158,9 +162,9 @@ class ReorderFlexState extends State<ReorderFlex>
void initState() { void initState() {
_notifier = ReorderFlexNotifier(); _notifier = ReorderFlexNotifier();
final flexId = widget.reorderFlexId; final flexId = widget.reorderFlexId;
dragState = widget.dragStateStorage?.readState(flexId) ?? draggingState = widget.dragStateStorage?.readState(flexId) ??
DraggingState(widget.reorderFlexId); DraggingState(widget.reorderFlexId);
Log.trace('[DragTarget] init dragState: $dragState'); Log.trace('[DragTarget] init dragState: $draggingState');
widget.dragStateStorage?.removeState(flexId); widget.dragStateStorage?.removeState(flexId);
@ -168,7 +172,7 @@ class ReorderFlexState extends State<ReorderFlex>
reorderAnimationDuration: widget.config.reorderAnimationDuration, reorderAnimationDuration: widget.config.reorderAnimationDuration,
entranceAnimateStatusChanged: (status) { entranceAnimateStatusChanged: (status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
if (dragState.nextIndex == -1) return; if (draggingState.nextIndex == -1) return;
setState(() => _requestAnimationToNextIndex()); setState(() => _requestAnimationToNextIndex());
} }
}, },
@ -225,7 +229,7 @@ class ReorderFlexState extends State<ReorderFlex>
indexKey, indexKey,
); );
children.add(_wrap(child, i, indexKey)); children.add(_wrap(child, i, indexKey, item.draggable));
// if (widget.config.useMovePlaceholder) { // if (widget.config.useMovePlaceholder) {
// children.add(DragTargeMovePlaceholder( // children.add(DragTargeMovePlaceholder(
@ -256,64 +260,70 @@ class ReorderFlexState extends State<ReorderFlex>
/// when the animation finish. /// when the animation finish.
if (_animation.entranceController.isCompleted) { if (_animation.entranceController.isCompleted) {
dragState.removePhantom(); draggingState.removePhantom();
if (!isAcceptingNewTarget && dragState.didDragTargetMoveToNext()) { if (!isAcceptingNewTarget && draggingState.didDragTargetMoveToNext()) {
return; return;
} }
dragState.moveDragTargetToNext(); draggingState.moveDragTargetToNext();
_animation.animateToNext(); _animation.animateToNext();
} }
} }
/// [child]: the child will be wrapped with dartTarget /// [child]: the child will be wrapped with dartTarget
/// [childIndex]: the index of the child in a list /// [childIndex]: the index of the child in a list
Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) { Widget _wrap(
Widget child,
int childIndex,
GlobalObjectKey indexKey,
IsDraggable draggable,
) {
return Builder(builder: (context) { return Builder(builder: (context) {
final ReorderDragTarget dragTarget = _buildDragTarget( final ReorderDragTarget dragTarget = _buildDragTarget(
context, context,
child, child,
childIndex, childIndex,
indexKey, indexKey,
draggable,
); );
int shiftedIndex = childIndex; int shiftedIndex = childIndex;
if (dragState.isOverlapWithPhantom()) { if (draggingState.isOverlapWithPhantom()) {
shiftedIndex = dragState.calculateShiftedIndex(childIndex); shiftedIndex = draggingState.calculateShiftedIndex(childIndex);
} }
Log.trace( Log.trace(
'Rebuild: Group:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); 'Rebuild: Group:[${draggingState.reorderFlexId}] ${draggingState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
final currentIndex = dragState.currentIndex; final currentIndex = draggingState.currentIndex;
final dragPhantomIndex = dragState.phantomIndex; final dragPhantomIndex = draggingState.phantomIndex;
if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) { if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) {
Widget dragSpace; Widget dragSpace;
if (dragState.draggingWidget != null) { if (draggingState.draggingWidget != null) {
if (dragState.draggingWidget is PhantomWidget) { if (draggingState.draggingWidget is PhantomWidget) {
dragSpace = dragState.draggingWidget!; dragSpace = draggingState.draggingWidget!;
} else { } else {
dragSpace = PhantomWidget( dragSpace = PhantomWidget(
opacity: widget.config.draggingWidgetOpacity, opacity: widget.config.draggingWidgetOpacity,
child: dragState.draggingWidget, child: draggingState.draggingWidget,
); );
} }
} else { } else {
dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize); dragSpace = SizedBox.fromSize(size: draggingState.dropAreaSize);
} }
/// Returns the dragTarget it is not start dragging. The size of the /// Returns the dragTarget it is not start dragging. The size of the
/// dragTarget is the same as the the passed in child. /// dragTarget is the same as the the passed in child.
/// ///
if (dragState.isNotDragging()) { if (draggingState.isNotDragging()) {
return _buildDraggingContainer(children: [dragTarget]); return _buildDraggingContainer(children: [dragTarget]);
} }
/// Determine the size of the drop area to show under the dragging widget. /// Determine the size of the drop area to show under the dragging widget.
Size? feedbackSize = Size.zero; Size? feedbackSize = Size.zero;
if (widget.config.useMoveAnimation) { if (widget.config.useMoveAnimation) {
feedbackSize = dragState.feedbackSize; feedbackSize = draggingState.feedbackSize;
} }
Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize); Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
@ -321,7 +331,7 @@ class ReorderFlexState extends State<ReorderFlex>
/// When start dragging, the dragTarget, [ReorderDragTarget], will /// When start dragging, the dragTarget, [ReorderDragTarget], will
/// return a [IgnorePointerWidget] which size is zero. /// return a [IgnorePointerWidget] which size is zero.
if (dragState.isPhantomAboveDragTarget()) { if (draggingState.isPhantomAboveDragTarget()) {
_notifier.updateDragTargetIndex(currentIndex); _notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) { if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [ return _buildDraggingContainer(children: [
@ -343,7 +353,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
/// ///
if (dragState.isPhantomBelowDragTarget()) { if (draggingState.isPhantomBelowDragTarget()) {
_notifier.updateDragTargetIndex(currentIndex); _notifier.updateDragTargetIndex(currentIndex);
if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) { if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
return _buildDraggingContainer(children: [ return _buildDraggingContainer(children: [
@ -364,10 +374,10 @@ class ReorderFlexState extends State<ReorderFlex>
} }
} }
assert(!dragState.isOverlapWithPhantom()); assert(!draggingState.isOverlapWithPhantom());
List<Widget> children = []; List<Widget> children = [];
if (dragState.isDragTargetMovingDown()) { if (draggingState.isDragTargetMovingDown()) {
children.addAll([dragTarget, appearSpace]); children.addAll([dragTarget, appearSpace]);
} else { } else {
children.addAll([appearSpace, dragTarget]); children.addAll([appearSpace, dragTarget]);
@ -395,15 +405,17 @@ class ReorderFlexState extends State<ReorderFlex>
Widget child, Widget child,
int dragTargetIndex, int dragTargetIndex,
GlobalObjectKey indexKey, GlobalObjectKey indexKey,
IsDraggable draggable,
) { ) {
final reorderFlexItem = widget.dataSource.items[dragTargetIndex]; final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
return ReorderDragTarget<FlexDragTargetData>( return ReorderDragTarget<FlexDragTargetData>(
indexGlobalKey: indexKey, indexGlobalKey: indexKey,
draggable: draggable,
dragTargetData: FlexDragTargetData( dragTargetData: FlexDragTargetData(
draggingIndex: dragTargetIndex, draggingIndex: dragTargetIndex,
reorderFlexId: widget.reorderFlexId, reorderFlexId: widget.reorderFlexId,
reorderFlexItem: reorderFlexItem, reorderFlexItem: reorderFlexItem,
state: dragState, draggingState: draggingState,
dragTargetId: reorderFlexItem.id, dragTargetId: reorderFlexItem.id,
dragTargetIndexKey: indexKey, dragTargetIndexKey: indexKey,
), ),
@ -432,11 +444,11 @@ class ReorderFlexState extends State<ReorderFlex>
setState(() { setState(() {
if (dragTargetData.reorderFlexId == widget.reorderFlexId) { if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
_onReordered( _onReordered(
dragState.dragStartIndex, draggingState.dragStartIndex,
dragState.currentIndex, draggingState.currentIndex,
); );
} }
dragState.endDragging(); draggingState.endDragging();
widget.onDragEnded?.call(); widget.onDragEnded?.call();
}); });
}, },
@ -482,8 +494,8 @@ class ReorderFlexState extends State<ReorderFlex>
deleteAnimationController: _animation.deleteController, deleteAnimationController: _animation.deleteController,
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
useMoveAnimation: widget.config.useMoveAnimation, useMoveAnimation: widget.config.useMoveAnimation,
draggable: widget.reorderable,
draggingOpacity: widget.config.draggingWidgetOpacity, draggingOpacity: widget.config.draggingWidgetOpacity,
dragDirection: widget.config.dragDirection,
child: child, child: child,
); );
} }
@ -506,7 +518,7 @@ class ReorderFlexState extends State<ReorderFlex>
child, child,
_animation.entranceController, _animation.entranceController,
feedbackSize, feedbackSize,
widget.direction, widget.config.direction,
); );
} }
@ -515,7 +527,7 @@ class ReorderFlexState extends State<ReorderFlex>
child, child,
_animation.phantomController, _animation.phantomController,
feedbackSize, feedbackSize,
widget.direction, widget.config.direction,
); );
} }
@ -525,7 +537,7 @@ class ReorderFlexState extends State<ReorderFlex>
Size? feedbackSize, Size? feedbackSize,
) { ) {
setState(() { setState(() {
dragState.startDragging(draggingWidget, dragIndex, feedbackSize); draggingState.startDragging(draggingWidget, dragIndex, feedbackSize);
_animation.startDragging(); _animation.startDragging();
}); });
} }
@ -535,34 +547,34 @@ class ReorderFlexState extends State<ReorderFlex>
return; return;
} }
dragState.setStartDraggingIndex(dragTargetIndex); draggingState.setStartDraggingIndex(dragTargetIndex);
widget.dragStateStorage?.insertState( widget.dragStateStorage?.insertState(
widget.reorderFlexId, widget.reorderFlexId,
dragState, draggingState,
); );
} }
bool handleOnWillAccept(BuildContext context, int dragTargetIndex) { bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
final dragIndex = dragState.dragStartIndex; final dragIndex = draggingState.dragStartIndex;
/// The [willAccept] will be true if the dargTarget is the widget that gets /// The [willAccept] will be true if the dargTarget is the widget that gets
/// dragged and it is dragged on top of the other dragTargets. /// dragged and it is dragged on top of the other dragTargets.
/// ///
bool willAccept = bool willAccept = draggingState.dragStartIndex == dragIndex &&
dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex; dragIndex != dragTargetIndex;
setState(() { setState(() {
if (willAccept) { if (willAccept) {
int shiftedIndex = dragState.calculateShiftedIndex(dragTargetIndex); int shiftedIndex = draggingState.calculateShiftedIndex(dragTargetIndex);
dragState.updateNextIndex(shiftedIndex); draggingState.updateNextIndex(shiftedIndex);
} else { } else {
dragState.updateNextIndex(dragTargetIndex); draggingState.updateNextIndex(dragTargetIndex);
} }
_requestAnimationToNextIndex(isAcceptingNewTarget: true); _requestAnimationToNextIndex(isAcceptingNewTarget: true);
}); });
Log.trace( Log.trace(
'[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}'); '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $draggingState}');
_scrollTo(context); _scrollTo(context);
@ -587,7 +599,7 @@ class ReorderFlexState extends State<ReorderFlex>
return child; return child;
} else { } else {
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: widget.direction, scrollDirection: widget.config.direction,
controller: _scrollController, controller: _scrollController,
child: child, child: child,
); );
@ -595,7 +607,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
Widget _wrapContainer(List<Widget> children) { Widget _wrapContainer(List<Widget> children) {
switch (widget.direction) { switch (widget.config.direction) {
case Axis.horizontal: case Axis.horizontal:
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -613,7 +625,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
Widget _buildDraggingContainer({required List<Widget> children}) { Widget _buildDraggingContainer({required List<Widget> children}) {
switch (widget.direction) { switch (widget.config.direction) {
case Axis.horizontal: case Axis.horizontal:
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -660,6 +672,7 @@ class ReorderFlexState extends State<ReorderFlex>
.ensureVisible( .ensureVisible(
dragTargetRenderObject, dragTargetRenderObject,
alignment: 0.5, alignment: 0.5,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
duration: const Duration(milliseconds: 120), duration: const Duration(milliseconds: 120),
) )
.then((value) { .then((value) {
@ -683,9 +696,9 @@ class ReorderFlexState extends State<ReorderFlex>
// If and only if the current scroll offset falls in-between the offsets // If and only if the current scroll offset falls in-between the offsets
// necessary to reveal the selected context at the top or bottom of the // necessary to reveal the selected context at the top or bottom of the
// screen, then it is already on-screen. // screen, then it is already on-screen.
final double margin = widget.direction == Axis.horizontal final double margin = widget.config.direction == Axis.horizontal
? dragState.dropAreaSize.width ? draggingState.dropAreaSize.width
: dragState.dropAreaSize.height / 2.0; : draggingState.dropAreaSize.height / 2.0;
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
final double scrollOffset = _scrollController.offset; final double scrollOffset = _scrollController.offset;
final double topOffset = max( final double topOffset = max(

View File

@ -46,15 +46,23 @@ class BoardPhantomController extends OverlapDragTargetDelegate
required this.groupsState, required this.groupsState,
}); });
bool isFromGroup(String groupId) { /// Determines whether the group should perform reorder
///
/// Returns `true` if the fromGroupId and toGroupId of the phantomRecord
/// equal to the passed in groupId.
///
/// Returns `true` if the phantomRecord is null
///
bool shouldReorder(String groupId) {
if (phantomRecord != null) { if (phantomRecord != null) {
return phantomRecord!.fromGroupId == groupId; return phantomRecord!.toGroupId == groupId &&
phantomRecord!.fromGroupId == groupId;
} else { } else {
return true; return true;
} }
} }
void transformIndex(int fromIndex, int toIndex) { void updateIndex(int fromIndex, int toIndex) {
if (phantomRecord == null) { if (phantomRecord == null) {
return; return;
} }
@ -69,7 +77,6 @@ class BoardPhantomController extends OverlapDragTargetDelegate
/// Remove the phantom in the group when the group is end dragging. /// Remove the phantom in the group when the group is end dragging.
void groupEndDragging(String groupId) { void groupEndDragging(String groupId) {
phantomState.setGroupIsDragging(groupId, false); phantomState.setGroupIsDragging(groupId, false);
if (phantomRecord == null) return; if (phantomRecord == null) return;
final fromGroupId = phantomRecord!.fromGroupId; final fromGroupId = phantomRecord!.fromGroupId;
@ -246,10 +253,6 @@ class PhantomRecord {
}); });
void updateFromGroupIndex(int index) { void updateFromGroupIndex(int index) {
if (fromGroupIndex == index) {
return;
}
fromGroupIndex = index; fromGroupIndex = index;
} }

View File

@ -1,6 +1,6 @@
name: appflowy_board name: appflowy_board
description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups.
version: 0.0.8 version: 0.0.9
homepage: https://github.com/AppFlowy-IO/AppFlowy homepage: https://github.com/AppFlowy-IO/AppFlowy
repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board

View File

@ -1,3 +1,13 @@
## 0.0.6
* Add three plugins: Code Block, LateX, and Horizontal rule.
* Support web platform.
* Support more markdown syntax conversions.
* `~ ~` to format text as strikethrough
* `_ _` to format text as italic
* \` \` to format text as code
* `[]()` to format text as link
* Fix some bugs.
## 0.0.5 ## 0.0.5
* Support customize the hotkeys for a shortcut on different platforms. * Support customize the hotkeys for a shortcut on different platforms.
* Support customize a theme. * Support customize a theme.

View File

@ -9,12 +9,6 @@
"align": "center" "align": "center"
} }
}, },
{
"type": "tex",
"attributes": {
"tex": "x = 2"
}
},
{ {
"type": "text", "type": "text",
"attributes": { "subtype": "heading", "heading": "h1" }, "attributes": { "subtype": "heading", "heading": "h1" },

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:example/plugin/code_block_node_widget.dart'; import 'package:example/plugin/code_block_node_widget.dart';
import 'package:example/plugin/horizontal_rule_node_widget.dart';
import 'package:example/plugin/tex_block_node_widget.dart'; import 'package:example/plugin/tex_block_node_widget.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -121,15 +122,18 @@ class _MyHomePageState extends State<MyHomePage> {
customBuilders: { customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(), 'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(), 'tex': TeXBlockNodeWidgetBuidler(),
'horizontal_rule': HorizontalRuleWidgetBuilder(),
}, },
shortcutEvents: [ shortcutEvents: [
enterInCodeBlock, enterInCodeBlock,
ignoreKeysInCodeBlock, ignoreKeysInCodeBlock,
underscoreToItalic, underscoreToItalic,
insertHorizontalRule,
], ],
selectionMenuItems: [ selectionMenuItems: [
codeBlockItem, codeBlockMenuItem,
teXBlockMenuItem, teXBlockMenuItem,
horizontalRuleMenuItem,
], ],
), ),
); );

View File

@ -44,9 +44,13 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
}; };
SelectionMenuItem codeBlockItem = SelectionMenuItem( SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: () => 'Code Block', name: () => 'Code Block',
icon: const Icon(Icons.abc), icon: const Icon(
Icons.abc,
color: Colors.black,
size: 18.0,
),
keywords: ['code block'], keywords: ['code block'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
final selection = final selection =

View File

@ -0,0 +1,167 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
ShortcutEvent insertHorizontalRule = ShortcutEvent(
key: 'Horizontal rule',
command: 'Minus',
handler: _insertHorzaontalRule,
);
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (textNodes.length != 1 || selection == null) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
if (textNode.toRawString() == '--') {
TransactionBuilder(editorState)
..deleteText(textNode, 0, 2)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: () => 'Horizontal rule',
icon: const Icon(
Icons.horizontal_rule,
color: Colors.black,
size: 18.0,
),
keywords: ['horizontal rule'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || textNodes.isEmpty) {
return;
}
final textNode = textNodes.first;
if (textNode.toRawString().isEmpty) {
TransactionBuilder(editorState)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
} else {
TransactionBuilder(editorState)
..insertNode(
selection.end.path.next,
TextNode(
type: 'text',
children: LinkedList(),
attributes: {
'subtype': 'horizontal_rule',
},
delta: Delta()..insert('---'),
),
)
..afterSelection = selection
..commit();
}
},
);
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _HorizontalRuleWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class _HorizontalRuleWidget extends StatefulWidget {
const _HorizontalRuleWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
}
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
with SelectableMixin {
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
height: 1,
color: Colors.grey,
),
);
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.borderLine;
@override
Rect? getCursorRectInPosition(Position position) {
final size = _renderBox.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}

View File

@ -6,7 +6,11 @@ import 'package:flutter_math_fork/flutter_math.dart';
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
name: () => 'Tex', name: () => 'Tex',
icon: const Icon(Icons.text_fields_rounded), icon: const Icon(
Icons.text_fields_rounded,
color: Colors.black,
size: 18.0,
),
keywords: ['tex, latex, katex'], keywords: ['tex, latex, katex'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
final selection = final selection =

View File

@ -24,7 +24,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View File

@ -0,0 +1,35 @@
{
"@@locale": "ml_IN",
"bold": "ബോൾഡ്",
"@bold": {},
"bulletedList": "ബുള്ളറ്റഡ് പട്ടിക",
"@bulletedList": {},
"checkbox": "ചെക്ക്ബോക്സ്",
"@checkbox": {},
"embedCode": "എംബെഡഡ് കോഡ്",
"@embedCode": {},
"heading1": "തലക്കെട്ട് 1",
"@heading1": {},
"heading2": "തലക്കെട്ട് 2",
"@heading2": {},
"heading3": "തലക്കെട്ട് 3",
"@heading3": {},
"highlight": "പ്രമുഖമാക്കിക്കാട്ടുക",
"@highlight": {},
"image": "ചിത്രം",
"@image": {},
"italic": "ഇറ്റാലിക്",
"@italic": {},
"link": "ലിങ്ക്",
"@link": {},
"numberedList": "അക്കമിട്ട പട്ടിക",
"@numberedList": {},
"quote": "ഉദ്ധരണി",
"@quote": {},
"strikethrough": "സ്ട്രൈക്ക്ത്രൂ",
"@strikethrough": {},
"text": "വചനം",
"@text": {},
"underline": "അടിവരയിടുക",
"@underline": {}
}

View File

@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode {
} }
final length = op.length; final length = op.length;
if (start < endOffset && start + length > startOffset) { if (start < endOffset && start + length > startOffset) {
if (op.attributes?.containsKey(styleKey) == true) { final attributes = op.attributes;
return op.attributes![styleKey]; if (attributes != null && attributes[styleKey] is T?) {
return attributes[styleKey];
} }
} }
start += length; start += length;
@ -29,42 +30,40 @@ extension TextNodeExtension on TextNode {
} }
bool allSatisfyLinkInSelection(Selection selection) => bool allSatisfyLinkInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.href, <bool>(value) { allSatisfyInSelection(selection, BuiltInAttributeKey.href, (value) {
return value != null; return value != null;
}); });
bool allSatisfyBoldInSelection(Selection selection) => bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, <bool>(value) { allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
return value == true; return value == true;
}); });
bool allSatisfyItalicInSelection(Selection selection) => bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.italic, allSatisfyInSelection(selection, BuiltInAttributeKey.italic, (value) {
<bool>(value) {
return value == true; return value == true;
}); });
bool allSatisfyUnderlineInSelection(Selection selection) => bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.underline, allSatisfyInSelection(selection, BuiltInAttributeKey.underline, (value) {
<bool>(value) {
return value == true; return value == true;
}); });
bool allSatisfyStrikethroughInSelection(Selection selection) => bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough, allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough,
<bool>(value) { (value) {
return value == true; return value == true;
}); });
bool allSatisfyCodeInSelection(Selection selection) => bool allSatisfyCodeInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.code, <bool>(value) { allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
return value == true; return value == true;
}); });
bool allSatisfyInSelection( bool allSatisfyInSelection(
Selection selection, Selection selection,
String styleKey, String styleKey,
bool Function<T>(T value) test, bool Function(dynamic value) test,
) { ) {
if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) { if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) {
if (attributes.containsKey(styleKey)) { if (attributes.containsKey(styleKey)) {
@ -129,40 +128,40 @@ extension TextNodesExtension on List<TextNode> {
bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection( bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.bold, BuiltInAttributeKey.bold,
<bool>(value) => value == true, (value) => value == true,
); );
bool allSatisfyItalicInSelection(Selection selection) => bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection( allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.italic, BuiltInAttributeKey.italic,
<bool>(value) => value == true, (value) => value == true,
); );
bool allSatisfyUnderlineInSelection(Selection selection) => bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection( allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.underline, BuiltInAttributeKey.underline,
<bool>(value) => value == true, (value) => value == true,
); );
bool allSatisfyStrikethroughInSelection(Selection selection) => bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection( allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.strikethrough, BuiltInAttributeKey.strikethrough,
<bool>(value) => value == true, (value) => value == true,
); );
bool allSatisfyInSelection( bool allSatisfyInSelection(
Selection selection, Selection selection,
String styleKey, String styleKey,
bool Function<T>(T value) test, bool Function(dynamic value) test,
) { ) {
if (isEmpty) { if (isEmpty) {
return false; return false;
} }
if (length == 1) { if (length == 1) {
return first.allSatisfyInSelection(selection, styleKey, <bool>(value) { return first.allSatisfyInSelection(selection, styleKey, (value) {
return test(value); return test(value);
}); });
} else { } else {

View File

@ -26,6 +26,7 @@ import 'messages_hu-HU.dart' as messages_hu_hu;
import 'messages_id-ID.dart' as messages_id_id; import 'messages_id-ID.dart' as messages_id_id;
import 'messages_it-IT.dart' as messages_it_it; import 'messages_it-IT.dart' as messages_it_it;
import 'messages_ja-JP.dart' as messages_ja_jp; import 'messages_ja-JP.dart' as messages_ja_jp;
import 'messages_ml_IN.dart' as messages_ml_in;
import 'messages_nl-NL.dart' as messages_nl_nl; import 'messages_nl-NL.dart' as messages_nl_nl;
import 'messages_pl-PL.dart' as messages_pl_pl; import 'messages_pl-PL.dart' as messages_pl_pl;
import 'messages_pt-BR.dart' as messages_pt_br; import 'messages_pt-BR.dart' as messages_pt_br;
@ -48,6 +49,7 @@ Map<String, LibraryLoader> _deferredLibraries = {
'id_ID': () => new Future.value(null), 'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null), 'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null), 'ja_JP': () => new Future.value(null),
'ml_IN': () => new Future.value(null),
'nl_NL': () => new Future.value(null), 'nl_NL': () => new Future.value(null),
'pl_PL': () => new Future.value(null), 'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null), 'pt_BR': () => new Future.value(null),
@ -82,6 +84,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
return messages_it_it.messages; return messages_it_it.messages;
case 'ja_JP': case 'ja_JP':
return messages_ja_jp.messages; return messages_ja_jp.messages;
case 'ml_IN':
return messages_ml_in.messages;
case 'nl_NL': case 'nl_NL':
return messages_nl_nl.messages; return messages_nl_nl.messages;
case 'pl_PL': case 'pl_PL':

View File

@ -0,0 +1,45 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ml_IN locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ml_IN';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("ബോൾഡ്"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("ബുള്ളറ്റഡ് പട്ടിക"),
"checkbox": MessageLookupByLibrary.simpleMessage("ചെക്ക്ബോക്സ്"),
"embedCode": MessageLookupByLibrary.simpleMessage("എംബെഡഡ് കോഡ്"),
"heading1": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 1"),
"heading2": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 2"),
"heading3": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 3"),
"highlight":
MessageLookupByLibrary.simpleMessage("പ്രമുഖമാക്കിക്കാട്ടുക"),
"image": MessageLookupByLibrary.simpleMessage("ചിത്രം"),
"italic": MessageLookupByLibrary.simpleMessage("ഇറ്റാലിക്"),
"link": MessageLookupByLibrary.simpleMessage("ലിങ്ക്"),
"numberedList":
MessageLookupByLibrary.simpleMessage("അക്കമിട്ട പട്ടിക"),
"quote": MessageLookupByLibrary.simpleMessage("ഉദ്ധരണി"),
"strikethrough": MessageLookupByLibrary.simpleMessage("സ്ട്രൈക്ക്ത്രൂ"),
"text": MessageLookupByLibrary.simpleMessage("വചനം"),
"underline": MessageLookupByLibrary.simpleMessage("അടിവരയിടുക")
};
}

View File

@ -229,6 +229,7 @@ class AppLocalizationDelegate
Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'), Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'), Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'),
Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'), Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'), Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CursorWidget extends StatefulWidget { class CursorWidget extends StatefulWidget {
@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget {
required this.rect, required this.rect,
required this.color, required this.color,
this.blinkingInterval = 0.5, this.blinkingInterval = 0.5,
this.shouldBlink = true,
this.cursorStyle = CursorStyle.verticalLine,
}) : super(key: key); }) : super(key: key);
final double blinkingInterval; // milliseconds final double blinkingInterval; // milliseconds
final bool shouldBlink;
final CursorStyle cursorStyle;
final Color color; final Color color;
final Rect rect; final Rect rect;
final LayerLink layerLink; final LayerLink layerLink;
@ -67,11 +72,28 @@ class CursorWidgetState extends State<CursorWidget> {
// Ignore the gestures in cursor // Ignore the gestures in cursor
// to solve the problem that cursor area cannot be selected. // to solve the problem that cursor area cannot be selected.
child: IgnorePointer( child: IgnorePointer(
child: Container( child: _buildCursor(context),
color: showCursor ? widget.color : Colors.transparent,
),
), ),
), ),
); );
} }
Widget _buildCursor(BuildContext context) {
var color = widget.color;
if (widget.shouldBlink && !showCursor) {
color = Colors.transparent;
}
switch (widget.cursorStyle) {
case CursorStyle.verticalLine:
return Container(
color: color,
);
case CursorStyle.borderLine:
return Container(
decoration: BoxDecoration(
border: Border.all(color: color, width: 2),
),
);
}
}
} }

View File

@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum CursorStyle {
verticalLine,
borderLine,
}
/// [SelectableMixin] is used for the editor to calculate the position /// [SelectableMixin] is used for the editor to calculate the position
/// and size of the selection. /// and size of the selection.
/// ///
@ -53,4 +58,8 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
Selection? getWorldBoundaryInOffset(Offset start) { Selection? getWorldBoundaryInOffset(Offset start) {
return null; return null;
} }
bool get shouldCursorBlink => true;
CursorStyle get cursorStyle => CursorStyle.verticalLine;
} }

View File

@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
typedef SelectionMenuItemHandler = void Function( typedef SelectionMenuItemHandler = void Function(
EditorState editorState, EditorState editorState,
SelectionMenuService menuService, SelectionMenuService menuService,
BuildContext context, BuildContext context,
); );
/// Selection Menu Item /// Selection Menu Item
class SelectionMenuItem { class SelectionMenuItem {
@ -23,7 +23,7 @@ class SelectionMenuItem {
this.handler = (editorState, menuService, context) { this.handler = (editorState, menuService, context) {
_deleteToSlash(editorState); _deleteToSlash(editorState);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
handler(editorState, menuService, context); handler(editorState, menuService, context);
}); });
}; };
} }

View File

@ -73,7 +73,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.heading, BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h1, (value) => value == BuiltInAttributeKey.h1,
), ),
handler: (editorState, context) => handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h1), formatHeading(editorState, BuiltInAttributeKey.h1),
@ -90,7 +90,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.heading, BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h2, (value) => value == BuiltInAttributeKey.h2,
), ),
handler: (editorState, context) => handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h2), formatHeading(editorState, BuiltInAttributeKey.h2),
@ -107,7 +107,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.heading, BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h3, (value) => value == BuiltInAttributeKey.h3,
), ),
handler: (editorState, context) => handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h3), formatHeading(editorState, BuiltInAttributeKey.h3),
@ -124,7 +124,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.bold, BuiltInAttributeKey.bold,
<bool>(value) => value == true, (value) => value == true,
), ),
handler: (editorState, context) => formatBold(editorState), handler: (editorState, context) => formatBold(editorState),
), ),
@ -140,7 +140,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.italic, BuiltInAttributeKey.italic,
<bool>(value) => value == true, (value) => value == true,
), ),
handler: (editorState, context) => formatItalic(editorState), handler: (editorState, context) => formatItalic(editorState),
), ),
@ -156,7 +156,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.underline, BuiltInAttributeKey.underline,
<bool>(value) => value == true, (value) => value == true,
), ),
handler: (editorState, context) => formatUnderline(editorState), handler: (editorState, context) => formatUnderline(editorState),
), ),
@ -172,7 +172,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.strikethrough, BuiltInAttributeKey.strikethrough,
<bool>(value) => value == true, (value) => value == true,
), ),
handler: (editorState, context) => formatStrikethrough(editorState), handler: (editorState, context) => formatStrikethrough(editorState),
), ),
@ -188,7 +188,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.code, BuiltInAttributeKey.code,
<bool>(value) => value == true, (value) => value == true,
), ),
handler: (editorState, context) => formatEmbedCode(editorState), handler: (editorState, context) => formatEmbedCode(editorState),
), ),
@ -204,7 +204,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.subtype, BuiltInAttributeKey.subtype,
<bool>(value) => value == BuiltInAttributeKey.quote, (value) => value == BuiltInAttributeKey.quote,
), ),
handler: (editorState, context) => formatQuote(editorState), handler: (editorState, context) => formatQuote(editorState),
), ),
@ -220,7 +220,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.subtype, BuiltInAttributeKey.subtype,
<bool>(value) => value == BuiltInAttributeKey.bulletedList, (value) => value == BuiltInAttributeKey.bulletedList,
), ),
handler: (editorState, context) => formatBulletedList(editorState), handler: (editorState, context) => formatBulletedList(editorState),
), ),
@ -236,7 +236,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.href, BuiltInAttributeKey.href,
<bool>(value) => value != null, (value) => value != null,
), ),
handler: (editorState, context) => showLinkMenu(context, editorState), handler: (editorState, context) => showLinkMenu(context, editorState),
), ),
@ -252,7 +252,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy( highlightCallback: (editorState) => _allSatisfy(
editorState, editorState,
BuiltInAttributeKey.backgroundColor, BuiltInAttributeKey.backgroundColor,
<bool>(value) => value != null, (value) => value != null,
), ),
handler: (editorState, context) => formatHighlight( handler: (editorState, context) => formatHighlight(
editorState, editorState,
@ -284,7 +284,7 @@ ToolbarItemValidator _showInBuiltInTextSelection = (editorState) {
bool _allSatisfy( bool _allSatisfy(
EditorState editorState, EditorState editorState,
String styleKey, String styleKey,
bool Function<T>(T value) test, bool Function(dynamic value) test,
) { ) {
final selection = editorState.service.selectionService.currentSelection.value; final selection = editorState.service.selectionService.currentSelection.value;
return selection != null && return selection != null &&

View File

@ -195,7 +195,7 @@ bool _allSatisfyInSelection(
return false; return false;
} }
return textNodes.allSatisfyInSelection(selection, styleKey, <bool>(value) { return textNodes.allSatisfyInSelection(selection, styleKey, (value) {
return value == matchValue; return value == matchValue;
}); });
} }

View File

@ -83,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
} }
} else { } else {
if (textNodes.isEmpty) { if (textNodes.isEmpty) {
if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection =
Selection.collapsed(selection.start);
}
transactionBuilder.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
final startPosition = selection.start; final startPosition = selection.start;

View File

@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
rect: cursorRect, rect: cursorRect,
color: widget.cursorColor, color: widget.cursorColor,
layerLink: node.layerLink, layerLink: node.layerLink,
shouldBlink: selectable.shouldCursorBlink,
cursorStyle: selectable.cursorStyle,
), ),
); );

View File

@ -1,12 +1,13 @@
name: appflowy_editor name: appflowy_editor
description: A highly customizable rich-text editor for Flutter description: A highly customizable rich-text editor for Flutter
version: 0.0.5 version: 0.0.6
homepage: https://github.com/AppFlowy-IO/AppFlowy homepage: https://github.com/AppFlowy-IO/AppFlowy
platforms: platforms:
linux: linux:
macos: macos:
windows: windows:
web:
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"

View File

@ -229,7 +229,7 @@ void main() async {
node.allSatisfyInSelection( node.allSatisfyInSelection(
code, code,
BuiltInAttributeKey.code, BuiltInAttributeKey.code,
<bool>(value) { (value) {
return value == true; return value == true;
}, },
), ),
@ -319,7 +319,7 @@ void main() async {
node.allSatisfyInSelection( node.allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.backgroundColor, BuiltInAttributeKey.backgroundColor,
<bool>(value) { (value) {
return value == blue; return value == blue;
}, },
), ),

View File

@ -111,7 +111,7 @@ Future<void> _testUpdateTextStyleByCommandX(
textNode.allSatisfyInSelection( textNode.allSatisfyInSelection(
selection, selection,
matchStyle, matchStyle,
<bool>(value) { (value) {
return value == matchValue; return value == matchValue;
}, },
), ),
@ -138,7 +138,7 @@ Future<void> _testUpdateTextStyleByCommandX(
textNode.allSatisfyInSelection( textNode.allSatisfyInSelection(
selection, selection,
matchStyle, matchStyle,
<bool>(value) { (value) {
return value == matchValue; return value == matchValue;
}, },
), ),
@ -192,7 +192,7 @@ Future<void> _testUpdateTextStyleByCommandX(
endOffset: text.length, endOffset: text.length,
), ),
matchStyle, matchStyle,
<bool>(value) { (value) {
return value == matchValue; return value == matchValue;
}, },
), ),
@ -266,7 +266,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
node.allSatisfyInSelection( node.allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.href, BuiltInAttributeKey.href,
<bool>(value) => value == link, (value) => value == link,
), ),
true); true);
@ -303,7 +303,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
node.allSatisfyInSelection( node.allSatisfyInSelection(
selection, selection,
BuiltInAttributeKey.href, BuiltInAttributeKey.href,
<bool>(value) => value == link, (value) => value == link,
), ),
false); false);
} }

View File

@ -2,6 +2,7 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra/time/duration.dart';
import 'package:flutter/services.dart';
class RoundedInputField extends StatefulWidget { class RoundedInputField extends StatefulWidget {
final String? hintText; final String? hintText;
@ -24,6 +25,7 @@ class RoundedInputField extends StatefulWidget {
final FocusNode? focusNode; final FocusNode? focusNode;
final TextEditingController? controller; final TextEditingController? controller;
final bool autoFocus; final bool autoFocus;
final int? maxLength;
const RoundedInputField({ const RoundedInputField({
Key? key, Key? key,
@ -47,6 +49,7 @@ class RoundedInputField extends StatefulWidget {
this.focusNode, this.focusNode,
this.controller, this.controller,
this.autoFocus = false, this.autoFocus = false,
this.maxLength,
}) : super(key: key); }) : super(key: key);
@override @override
@ -89,6 +92,9 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
initialValue: widget.initialValue, initialValue: widget.initialValue,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autoFocus, autofocus: widget.autoFocus,
maxLength: widget.maxLength,
maxLengthEnforcement:
MaxLengthEnforcement.truncateAfterCompositionEnds,
onChanged: (value) { onChanged: (value) {
inputText = value; inputText = value;
if (widget.onChanged != null) { if (widget.onChanged != null) {

View File

@ -623,7 +623,7 @@ impl GridRevisionEditor {
self.view_manager self.view_manager
.move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| { .move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| {
wrap_future(async move { wrap_future(async move {
tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); tracing::trace!("Row data changed: {:?}", row_changeset);
let cell_changesets = row_changeset let cell_changesets = row_changeset
.cell_by_field_id .cell_by_field_id
.into_iter() .into_iter()

View File

@ -79,7 +79,7 @@ impl GridViewRevisionEditor {
Ok(json_str) Ok(json_str)
} }
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { pub(crate) async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
if params.group_id.is_none() { if params.group_id.is_none() {
return; return;
} }
@ -92,7 +92,7 @@ impl GridViewRevisionEditor {
.await; .await;
} }
pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { pub(crate) async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
// Send the group notification if the current view has groups // Send the group notification if the current view has groups
match params.group_id.as_ref() { match params.group_id.as_ref() {
None => {} None => {}
@ -115,7 +115,7 @@ impl GridViewRevisionEditor {
} }
#[tracing::instrument(level = "trace", skip_all)] #[tracing::instrument(level = "trace", skip_all)]
pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) { pub(crate) async fn did_delete_view_row(&self, row_rev: &RowRevision) {
// Send the group notification if the current view has groups; // Send the group notification if the current view has groups;
let changesets = self let changesets = self
.mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev)) .mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev))
@ -129,7 +129,7 @@ impl GridViewRevisionEditor {
} }
} }
pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) { pub(crate) async fn did_update_view_row(&self, row_rev: &RowRevision) {
let changesets = self let changesets = self
.mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev)) .mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev))
.await; .await;
@ -141,7 +141,7 @@ impl GridViewRevisionEditor {
} }
} }
pub(crate) async fn move_group_row( pub(crate) async fn move_view_group_row(
&self, &self,
row_rev: &RowRevision, row_rev: &RowRevision,
row_changeset: &mut RowChangeset, row_changeset: &mut RowChangeset,
@ -167,14 +167,14 @@ impl GridViewRevisionEditor {
} }
/// Only call once after grid view editor initialized /// Only call once after grid view editor initialized
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> { pub(crate) async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = self.group_controller.read().await.groups(); let groups = self.group_controller.read().await.groups();
tracing::trace!("Number of groups: {}", groups.len()); tracing::trace!("Number of groups: {}", groups.len());
Ok(groups.into_iter().map(GroupPB::from).collect()) Ok(groups.into_iter().map(GroupPB::from).collect())
} }
#[tracing::instrument(level = "trace", skip(self), err)] #[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub(crate) async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self let _ = self
.group_controller .group_controller
.write() .write()
@ -206,13 +206,13 @@ impl GridViewRevisionEditor {
self.group_controller.read().await.field_id().to_owned() self.group_controller.read().await.field_id().to_owned()
} }
pub(crate) async fn get_setting(&self) -> GridSettingPB { pub(crate) async fn get_view_setting(&self) -> GridSettingPB {
let field_revs = self.field_delegate.get_field_revs().await; let field_revs = self.field_delegate.get_field_revs().await;
let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs); let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs);
grid_setting grid_setting
} }
pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfigurationPB> { pub(crate) async fn get_view_filters(&self) -> Vec<GridFilterConfigurationPB> {
let field_revs = self.field_delegate.get_field_revs().await; let field_revs = self.field_delegate.get_field_revs().await;
match self.pad.read().await.get_all_filters(&field_revs) { match self.pad.read().await.get_all_filters(&field_revs) {
None => vec![], None => vec![],
@ -245,7 +245,7 @@ impl GridViewRevisionEditor {
Ok(()) Ok(())
} }
pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> { pub(crate) async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self.modify(|pad| { self.modify(|pad| {
let changeset = pad.delete_filter(&params.field_id, &params.field_type_rev, &params.group_id)?; let changeset = pad.delete_filter(&params.field_id, &params.field_type_rev, &params.group_id)?;
Ok(changeset) Ok(changeset)
@ -253,7 +253,7 @@ impl GridViewRevisionEditor {
.await .await
} }
pub(crate) async fn insert_filter(&self, params: InsertFilterParams) -> FlowyResult<()> { pub(crate) async fn insert_view_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
self.modify(|pad| { self.modify(|pad| {
let filter_rev = FilterConfigurationRevision { let filter_rev = FilterConfigurationRevision {
id: gen_grid_filter_id(), id: gen_grid_filter_id(),
@ -267,7 +267,7 @@ impl GridViewRevisionEditor {
.await .await
} }
pub(crate) async fn delete_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> { pub(crate) async fn delete_view_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> {
self.modify(|pad| { self.modify(|pad| {
let changeset = pad.delete_filter( let changeset = pad.delete_filter(
&delete_filter.field_id, &delete_filter.field_id,
@ -324,7 +324,7 @@ impl GridViewRevisionEditor {
} }
async fn notify_did_update_setting(&self) { async fn notify_did_update_setting(&self) {
let setting = self.get_setting().await; let setting = self.get_view_setting().await;
send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting) send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
.payload(setting) .payload(setting)
.send(); .send();

View File

@ -65,14 +65,14 @@ impl GridViewManager {
/// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams]. /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
for view_editor in self.view_editors.iter() { for view_editor in self.view_editors.iter() {
view_editor.will_create_row(row_rev, params).await; view_editor.will_create_view_row(row_rev, params).await;
} }
} }
/// Notify the view that the row was created. For the moment, the view is just sending notifications. /// Notify the view that the row was created. For the moment, the view is just sending notifications.
pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
for view_editor in self.view_editors.iter() { for view_editor in self.view_editors.iter() {
view_editor.did_create_row(row_pb, params).await; view_editor.did_create_view_row(row_pb, params).await;
} }
} }
@ -84,7 +84,7 @@ impl GridViewManager {
} }
Some(row_rev) => { Some(row_rev) => {
for view_editor in self.view_editors.iter() { for view_editor in self.view_editors.iter() {
view_editor.did_update_row(&row_rev).await; view_editor.did_update_view_row(&row_rev).await;
} }
} }
} }
@ -102,33 +102,33 @@ impl GridViewManager {
pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) { pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
for view_editor in self.view_editors.iter() { for view_editor in self.view_editors.iter() {
view_editor.did_delete_row(&row_rev).await; view_editor.did_delete_view_row(&row_rev).await;
} }
} }
pub(crate) async fn get_setting(&self) -> FlowyResult<GridSettingPB> { pub(crate) async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
Ok(view_editor.get_setting().await) Ok(view_editor.get_view_setting().await)
} }
pub(crate) async fn get_filters(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> { pub(crate) async fn get_filters(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
Ok(view_editor.get_filters().await) Ok(view_editor.get_view_filters().await)
} }
pub(crate) async fn insert_or_update_filter(&self, params: InsertFilterParams) -> FlowyResult<()> { pub(crate) async fn insert_or_update_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
view_editor.insert_filter(params).await view_editor.insert_view_filter(params).await
} }
pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
view_editor.delete_filter(params).await view_editor.delete_view_filter(params).await
} }
pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> { pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
let groups = view_editor.load_groups().await?; let groups = view_editor.load_view_groups().await?;
Ok(RepeatedGridGroupPB { items: groups }) Ok(RepeatedGridGroupPB { items: groups })
} }
@ -139,12 +139,12 @@ impl GridViewManager {
pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> { pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
view_editor.delete_group(params).await view_editor.delete_view_group(params).await
} }
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
let _ = view_editor.move_group(params).await?; let _ = view_editor.move_view_group(params).await?;
Ok(()) Ok(())
} }
@ -161,7 +161,7 @@ impl GridViewManager {
let mut row_changeset = RowChangeset::new(row_rev.id.clone()); let mut row_changeset = RowChangeset::new(row_rev.id.clone());
let view_editor = self.get_default_view_editor().await?; let view_editor = self.get_default_view_editor().await?;
let group_changesets = view_editor let group_changesets = view_editor
.move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) .move_view_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
.await; .await;
if !row_changeset.is_empty() { if !row_changeset.is_empty() {

View File

@ -119,12 +119,20 @@ where
self.mut_configuration(|configuration| { self.mut_configuration(|configuration| {
let from_index = configuration.groups.iter().position(|group| group.id == from_id); let from_index = configuration.groups.iter().position(|group| group.id == from_id);
let to_index = configuration.groups.iter().position(|group| group.id == to_id); let to_index = configuration.groups.iter().position(|group| group.id == to_id);
tracing::info!("Configuration groups: {:?} ", configuration.groups);
if let (Some(from), Some(to)) = &(from_index, to_index) { if let (Some(from), Some(to)) = &(from_index, to_index) {
tracing::trace!("Move group from index:{:?} to index:{:?}", from_index, to_index); tracing::trace!("Move group from index:{:?} to index:{:?}", from_index, to_index);
let group = configuration.groups.remove(*from); let group = configuration.groups.remove(*from);
configuration.groups.insert(*to, group); configuration.groups.insert(*to, group);
} }
tracing::debug!(
"Group order: {:?} ",
configuration
.groups
.iter()
.map(|group| group.name.clone())
.collect::<Vec<String>>()
.join(",")
);
from_index.is_some() && to_index.is_some() from_index.is_some() && to_index.is_some()
})?; })?;

View File

@ -80,9 +80,9 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
}; };
// Remove the row in which group contains it // Remove the row in which group contains it
if from_index.is_some() { if let Some(from_index) = &from_index {
changeset.deleted_rows.push(row_rev.id.clone()); changeset.deleted_rows.push(row_rev.id.clone());
tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); tracing::debug!("Group:{} remove {} at {}", group.id, row_rev.id, from_index);
group.remove_row(&row_rev.id); group.remove_row(&row_rev.id);
} }
@ -97,10 +97,11 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
} }
Some(to_index) => { Some(to_index) => {
if to_index < group.number_of_row() { if to_index < group.number_of_row() {
tracing::debug!("Group:{} insert row:{} at {} ", group.id, row_rev.id, to_index); tracing::debug!("Group:{} insert {} at {} ", group.id, row_rev.id, to_index);
inserted_row.index = Some(to_index as i32); inserted_row.index = Some(to_index as i32);
group.insert_row(to_index, row_pb); group.insert_row(to_index, row_pb);
} else { } else {
tracing::warn!("Mote to index: {} is out of bounds", to_index);
tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
group.add_row(row_pb); group.add_row(row_pb);
} }

View File

@ -350,6 +350,36 @@ async fn group_move_from_default_group_test() {
#[tokio::test] #[tokio::test]
async fn group_move_group_test() { async fn group_move_group_test() {
let mut test = GridGroupTest::new().await;
let group_0 = test.group_at_index(0).await;
let group_1 = test.group_at_index(1).await;
let scripts = vec![
MoveGroup {
from_group_index: 0,
to_group_index: 1,
},
AssertGroupRowCount {
group_index: 0,
row_count: 2,
},
AssertGroup {
group_index: 0,
expected_group: group_1,
},
AssertGroupRowCount {
group_index: 1,
row_count: 2,
},
AssertGroup {
group_index: 1,
expected_group: group_0,
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn group_move_group_row_after_move_group_test() {
let mut test = GridGroupTest::new().await; let mut test = GridGroupTest::new().await;
let group_0 = test.group_at_index(0).await; let group_0 = test.group_at_index(0).await;
let group_1 = test.group_at_index(1).await; let group_1 = test.group_at_index(1).await;
@ -366,6 +396,20 @@ async fn group_move_group_test() {
group_index: 1, group_index: 1,
expected_group: group_0, expected_group: group_0,
}, },
MoveRow {
from_group_index: 0,
from_row_index: 0,
to_group_index: 1,
to_row_index: 0,
},
AssertGroupRowCount {
group_index: 0,
row_count: 1,
},
AssertGroupRowCount {
group_index: 1,
row_count: 3,
},
]; ];
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }

View File

@ -0,0 +1,2 @@
build-dir/
.flatpak-builder/

View File

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=AppFlowy
Icon=io.appflowy.AppFlowy
Exec=env GDK_GL=gles app_flowy %U
Categories=Network;Productivity;
Keywords=Notes

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.appflowy.AppFlowy</id>
<name>AppFlowy</name>
<summary>Open Source Notion Alternative</summary>
<metadata_license>CC-BY-4.0</metadata_license>
<project_license>AGPL-3.0-only</project_license>
<description>
<p>
# Built for teams that need more control and flexibility ## 100% data control You can host AppFlowy wherever you want; no vendor lock-in.
</p>
<p>
## Unlimited customizations Design and modify AppFlowy your way with an open core codebase.
</p>
<p>
## One codebase supporting multiple platforms AppFlowy is built with Flutter and Rust. What does this mean? Faster development, better native experience, and more reliable performance.
</p>
<p>
# Built for individuals who care about data security and mobile experience ## 100% control of your data Download and install AppFlowy on your local machine. You own and control your personal data.
</p>
<p>
## Extensively extensible For those with no coding experience, AppFlowy enables you to create apps that suit your needs. It&apos;s built on a community-driven toolbox, including templates, plugins, themes, and more.
</p>
<p>
## Truely native experience Faster, more stable with support for offline mode. It&apos;s also better integrated with different devices. Moreover, AppFlowy enables users to access features and possibilities not available on the web.
</p>
</description>
<launchable type="desktop-id">io.appflowy.AppFlowy.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://github.com/AppFlowy-IO/appflowy/raw/main/doc/imgs/welcome.png</image>
</screenshot>
</screenshots>
</component>

View File

@ -0,0 +1,35 @@
app-id: io.appflowy.AppFlowy
runtime: org.freedesktop.Platform
runtime-version: '21.08'
sdk: org.freedesktop.Sdk
command: app_flowy
separate-locales: false
finish-args:
- --share=ipc
- --socket=x11
- --socket=fallback-x11
- --socket=wayland
- --socket=pulseaudio
- --share=network
- --device=all
modules:
- name: appflowy
buildsystem: simple
build-commands:
# - ls .
- cp -r appflowy /app/appflowy
- chmod +x /app/appflowy/app_flowy
- install -Dm644 logo.svg /app/share/icons/hicolor/scalable/apps/io.appflowy.AppFlowy.svg
- mkdir /app/bin
- ln -s /app/appflowy/app_flowy /app/bin/app_flowy
- install -Dm644 io.appflowy.AppFlowy.desktop /app/share/applications/io.appflowy.AppFlowy.desktop
sources:
- type: archive
url: https://github.com/AppFlowy-IO/appflowy/releases/download/0.0.2/AppFlowy-linux-x86.tar.gz
sha256: b0dbe669bb9f34a65171adecaf61b02578bab5214d18a54009f0e4ec10665711
dest: appflowy
- type: file
path: io.appflowy.AppFlowy.desktop
- type: file
path: logo.svg

View File

@ -0,0 +1,11 @@
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M39.9564 24.0195C38.8098 30.1683 33.7828 35.5321 28.0061 38.5411C27.3005 38.9336 26.4627 39.1516 25.6689 39.1952H37.9279C39.1185 39.1952 39.9564 38.323 39.9564 37.2328V24.0195Z" fill="#F7931E"/>
<path d="M15.4381 12.1576C15.2617 12.2884 15.0853 12.4192 14.9089 12.55C11.9103 14.6432 2.82634 21.3589 0.753788 18.4371C-1.27467 15.6026 0.886079 7.57868 6.08952 3.69755C6.17771 3.61033 6.31 3.56672 6.3982 3.4795C12.0867 -0.48885 16.32 0.078058 18.3926 2.95621C20.3328 5.65992 18.1721 9.93353 15.4381 12.1576Z" fill="#8427E0"/>
<path d="M33.8715 36.098C33.7833 36.1852 33.6951 36.2288 33.5628 36.316C27.8743 40.2844 23.641 39.7175 21.5684 36.8393C19.6282 34.1356 21.7889 29.862 24.5229 27.638C24.6993 27.5072 24.8757 27.3763 25.0521 27.2455C28.0507 25.1959 37.1347 18.4366 39.1631 21.3584C41.2357 24.1929 39.119 32.2169 33.8715 36.098Z" fill="#FFBD00"/>
<path d="M17.9954 38.8459C15.085 40.8955 6.70658 38.6715 2.87014 33.264C2.78195 33.1768 2.69376 33.046 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C11.91 24.7168 11.9982 24.804 12.0864 24.9349C14.159 27.8566 20.9499 36.8399 17.9954 38.8459Z" fill="#E3006D"/>
<path d="M15.4385 12.1576C11.3816 13.9455 2.73857 17.6086 1.45976 14.6432C0.357338 12.1576 2.3858 7.09899 6.08994 3.69755C6.17814 3.61033 6.31043 3.56672 6.39862 3.4795C12.0871 -0.48885 16.3204 0.078058 18.393 2.95621C20.3333 5.65992 18.1725 9.93353 15.4385 12.1576Z" fill="#9327FF"/>
<path d="M37.6624 18.3955C34.8402 20.3579 30.4305 18.0903 28.2257 15.2557C28.1375 15.1249 28.0493 15.0377 27.9611 14.9069C25.8444 11.9415 19.0535 2.95819 21.9639 0.952211C24.8743 -1.09738 33.2968 1.12664 37.1333 6.53407C37.2215 6.6649 37.3096 6.75211 37.3978 6.88294C41.102 12.334 40.5287 16.3895 37.6624 18.3955Z" fill="#00B5FF"/>
<path d="M37.6628 18.3934C34.8406 20.3557 30.4309 18.0881 28.2261 15.2536C26.4181 11.1108 22.9344 2.95603 25.8448 1.73499C28.4906 0.601179 33.9587 2.86881 37.4423 6.88077C41.1024 12.3318 40.5291 16.3874 37.6628 18.3934Z" fill="#00C8FF"/>
<path d="M33.8715 36.0986C33.7833 36.1858 33.6951 36.2294 33.5628 36.3166C27.8743 40.285 23.641 39.7181 21.5684 36.8399C19.6282 34.1362 21.7889 29.8626 24.5229 27.6386C28.5799 25.8506 37.2229 22.1875 38.5017 25.1529C39.6482 27.6386 37.6197 32.6971 33.8715 36.0986Z" fill="#FFCE00"/>
<path d="M14.2031 38.061C11.5572 39.1948 6.08922 36.9708 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C13.6298 28.6852 17.1135 36.8399 14.2031 38.061Z" fill="#FB006D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB