feat: enable to reorder favorites (#7172)

This commit is contained in:
Morn 2025-01-09 14:33:53 +08:00 committed by GitHub
parent c3b702849f
commit 99a4e330e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 180 additions and 50 deletions

View File

@ -196,5 +196,58 @@ void main() {
await tester.pumpAndSettle();
},
);
testWidgets(
'reorder favorites',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// there are no favorite views
final favorites = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.byType(ViewItem),
);
expect(favorites, findsNothing);
/// create views and then favorite them
const pageNames = ['001', '002', '003'];
for (final name in pageNames) {
await tester.createNewPageWithNameUnderParent(name: name);
}
for (final name in pageNames) {
await tester.favoriteViewByName(name);
}
expect(favorites, findsNWidgets(pageNames.length));
final oldNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(oldNames, pageNames);
/// drag first to last
await tester.reorderFavorite(
fromName: '001',
toName: '003',
);
List<String> newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['002', '003', '001']);
/// drag first to second
await tester.reorderFavorite(
fromName: '002',
toName: '003',
);
newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['003', '002', '001']);
},
);
});
}

View File

@ -19,6 +19,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
@ -600,6 +601,23 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> reorderFavorite({
required String fromName,
required String toName,
}) async {
final from = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(fromName),
),
to = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(toName),
);
final distanceY = getCenter(to).dy - getCenter(from).dx;
await drag(from, Offset(0, distanceY));
await pumpAndSettle();
}
// tap the button with [FlowySvgData]
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
final button = find.byWidgetPredicate(

View File

@ -18,6 +18,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
final _service = FavoriteService();
final _listener = FavoriteListener();
bool isReordering = false;
@override
Future<void> close() async {
@ -68,10 +69,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
await _service.pinFavorite(view);
}
await _service.toggleFavorite(
view.id,
!view.isFavorite,
);
await _service.toggleFavorite(view.id);
},
pin: (view) async {
await _service.pinFavorite(view);
@ -81,6 +79,21 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
await _service.unpinFavorite(view);
add(const FavoriteEvent.fetchFavorites());
},
reorder: (oldIndex, newIndex) async {
/// TODO: this is a workaround to reorder the favorite views
isReordering = true;
final pinnedViews = state.pinnedViews.toList();
if (oldIndex < newIndex) newIndex -= 1;
final target = pinnedViews.removeAt(oldIndex);
pinnedViews.insert(newIndex, target);
emit(state.copyWith(pinnedViews: pinnedViews));
for (final view in pinnedViews) {
await _service.toggleFavorite(view.item.id);
await _service.toggleFavorite(view.item.id);
}
add(const FavoriteEvent.fetchFavorites());
isReordering = false;
},
);
},
);
@ -90,20 +103,29 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
FlowyResult<RepeatedViewPB, FlowyError> favoriteOrFailed,
bool didFavorite,
) {
favoriteOrFailed.fold(
(favorite) => add(const FetchFavorites()),
(error) => Log.error(error),
);
if (!isReordering) {
favoriteOrFailed.fold(
(favorite) => add(const FetchFavorites()),
(error) => Log.error(error),
);
}
}
}
@freezed
class FavoriteEvent with _$FavoriteEvent {
const factory FavoriteEvent.initial() = Initial;
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
const factory FavoriteEvent.fetchFavorites() = FetchFavorites;
const factory FavoriteEvent.pin(ViewPB view) = PinFavorite;
const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite;
const factory FavoriteEvent.reorder(int oldIndex, int newIndex) =
ReorderFavorite;
}
@freezed

View File

@ -25,10 +25,7 @@ class FavoriteService {
});
}
Future<FlowyResult<void, FlowyError>> toggleFavorite(
String viewId,
bool favoriteStatus,
) async {
Future<FlowyResult<void, FlowyError>> toggleFavorite(String viewId) async {
final id = RepeatedViewIdPB.create()..items.add(viewId);
return FolderEventToggleFavorite(id).send();
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
@ -54,8 +55,7 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
),
// pages
..._buildViews(context, state, isHovered),
buildReorderListView(context, state),
if (state.isExpanded) ...[
// more button
const VSpace(2),
@ -69,51 +69,91 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
);
}
Iterable<Widget> _buildViews(
Widget buildReorderListView(
BuildContext context,
FolderState state,
ValueNotifier<bool> isHovered,
) {
if (!state.isExpanded) {
return [];
if (!state.isExpanded) return const SizedBox.shrink();
final favoriteBloc = context.read<FavoriteBloc>();
final pinnedViews =
favoriteBloc.state.pinnedViews.map((e) => e.item).toList();
if (pinnedViews.isEmpty) return const SizedBox.shrink();
if (pinnedViews.length == 1) {
return buildViewItem(pinnedViews.first);
}
final pinnedViews =
context.read<FavoriteBloc>().state.pinnedViews.map((e) => e.item);
return pinnedViews.map(
(view) => ViewItem(
key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'),
spaceType: FolderSpaceType.favorite,
isDraggable: false,
isFirstChild: view.id == widget.views.first.id,
isFeedback: false,
view: view,
enableRightClickContext: true,
leftPadding: HomeSpaceViewSizes.leftPadding,
leftIconBuilder: (_, __) =>
const HSpace(HomeSpaceViewSizes.leftPadding),
level: 0,
isHovered: isHovered,
rightIconsBuilder: (context, view) => [
FavoriteMoreActions(view: view),
const HSpace(8.0),
FavoritePinAction(view: view),
const HSpace(4.0),
],
shouldRenderChildren: false,
shouldLoadChildViews: false,
onTertiarySelected: (_, view) => context.read<TabsBloc>().openTab(view),
onSelected: (_, view) {
if (HardwareKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
context.read<TabsBloc>().openPlugin(view);
return Theme(
data: Theme.of(context).copyWith(
canvasColor: Colors.transparent,
shadowColor: Colors.transparent,
),
child: ReorderableListView.builder(
shrinkWrap: true,
buildDefaultDragHandles: false,
itemCount: pinnedViews.length,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
final view = pinnedViews[i];
return ReorderableDragStartListener(
key: ValueKey(view.id),
index: i,
child: DecoratedBox(
decoration: const BoxDecoration(color: Colors.transparent),
child: buildViewItem(view),
),
);
},
onReorder: (oldIndex, newIndex) {
favoriteBloc.add(FavoriteEvent.reorder(oldIndex, newIndex));
},
),
);
}
Widget buildViewItem(ViewPB view) {
return ViewItem(
key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'),
spaceType: FolderSpaceType.favorite,
isDraggable: false,
isFirstChild: view.id == widget.views.first.id,
isFeedback: false,
view: view,
enableRightClickContext: true,
leftPadding: HomeSpaceViewSizes.leftPadding,
leftIconBuilder: (_, __) => const HSpace(HomeSpaceViewSizes.leftPadding),
level: 0,
isHovered: isHovered,
rightIconsBuilder: (context, view) => [
Listener(
child: FavoriteMoreActions(view: view),
onPointerDown: (e) {
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));
},
),
const HSpace(8.0),
Listener(
child: FavoritePinAction(view: view),
onPointerDown: (e) {
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));
},
),
const HSpace(4.0),
],
shouldRenderChildren: false,
shouldLoadChildViews: false,
onTertiarySelected: (_, view) => context.read<TabsBloc>().openTab(view),
onSelected: (_, view) {
if (HardwareKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
context.read<TabsBloc>().openPlugin(view);
},
);
}
}
class FavoriteHeader extends StatelessWidget {