mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support favorites folder
This commit is contained in:
parent
ff79635b2b
commit
a1143e24f3
@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 6L18.781 11.9243L25 12.8801L20.5 17.489L21.562 24L16 20.9243L10.438 24L11.5 17.489L7 12.8801L13.219 11.9243L16 6Z" fill="#FFD667" stroke="#FFD667" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 329 B |
@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 6L18.781 11.9243L25 12.8801L20.5 17.489L21.562 24L16 20.9243L10.438 24L11.5 17.489L7 12.8801L13.219 11.9243L16 6Z" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 314 B |
@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4.5L14.0858 8.94322L18.75 9.66009L15.375 13.1167L16.1715 18L12 15.6932L7.8285 18L8.625 13.1167L5.25 9.66009L9.91425 8.94322L12 4.5Z" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" fill="#FFD667" stroke="#FFD667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 315 B |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" fill="#none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 313 B |
@ -0,0 +1,48 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('sidebar expand test', () {
|
||||
bool isExpanded({required FolderCategoryType type}) {
|
||||
if (type == FolderCategoryType.personal) {
|
||||
return find
|
||||
.descendant(
|
||||
of: find.byType(PersonalFolder),
|
||||
matching: find.byType(ViewItem),
|
||||
)
|
||||
.evaluate()
|
||||
.isNotEmpty;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
testWidgets('first time the personal folder is expanded', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// first time is expanded
|
||||
expect(isExpanded(type: FolderCategoryType.personal), true);
|
||||
|
||||
// collapse the personal folder
|
||||
await tester.tapButton(
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
|
||||
);
|
||||
expect(isExpanded(type: FolderCategoryType.personal), false);
|
||||
|
||||
// expand the personal folder
|
||||
await tester.tapButton(
|
||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
|
||||
);
|
||||
expect(isExpanded(type: FolderCategoryType.personal), true);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/base.dart';
|
||||
import '../util/common_operations.dart';
|
||||
import '../util/expectation.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Favorites', () {
|
||||
testWidgets(
|
||||
'Toggle favorites for views creates / removes the favorite header along with favorite views',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// no favorite folder
|
||||
expect(find.byType(FavoriteFolder), findsNothing);
|
||||
|
||||
// create the nested views
|
||||
final names = [
|
||||
1,
|
||||
2,
|
||||
].map((e) => 'document_$e').toList();
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
final parentName = i == 0 ? gettingStated : names[i - 1];
|
||||
await tester.createNewPageWithName(
|
||||
name: names[i],
|
||||
parentName: parentName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
tester.expectToSeePageName(
|
||||
names[i],
|
||||
parentName: parentName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
parentLayout: ViewLayoutPB.Document,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.favoriteViewByName(gettingStated);
|
||||
expect(
|
||||
tester.findFavoritePageName(gettingStated),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.favoriteViewByName(names[1]);
|
||||
expect(
|
||||
tester.findFavoritePageName(names[1]),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
await tester.unfavoriteViewByName(gettingStated);
|
||||
expect(
|
||||
tester.findFavoritePageName(gettingStated),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
tester.findFavoritePageName(
|
||||
names[1],
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.unfavoriteViewByName(names[1]);
|
||||
expect(
|
||||
tester.findFavoritePageName(
|
||||
names[1],
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'renaming a favorite view updates name under favorite header',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
const name = 'test';
|
||||
await tester.favoriteViewByName(gettingStated);
|
||||
await tester.hoverOnPageName(
|
||||
gettingStated,
|
||||
layout: ViewLayoutPB.Document,
|
||||
onHover: () async {
|
||||
await tester.renamePage(name);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
expect(
|
||||
tester.findPageName(name),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
expect(
|
||||
tester.findFavoritePageName(name),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'deleting first level favorite view removes its instance from favorite header, deleting root level views leads to removal of all favorites that are its children',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final names = [1, 2].map((e) => 'document_$e').toList();
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
final parentName = i == 0 ? gettingStated : names[i - 1];
|
||||
await tester.createNewPageWithName(
|
||||
name: names[i],
|
||||
parentName: parentName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
tester.expectToSeePageName(names[i], parentName: parentName);
|
||||
}
|
||||
await tester.favoriteViewByName(gettingStated);
|
||||
await tester.favoriteViewByName(names[0]);
|
||||
await tester.favoriteViewByName(names[1]);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
findsNWidgets(6),
|
||||
);
|
||||
|
||||
await tester.hoverOnPageName(
|
||||
names[1],
|
||||
layout: ViewLayoutPB.Document,
|
||||
onHover: () async {
|
||||
await tester.tapDeletePageButton();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
|
||||
await tester.hoverOnPageName(
|
||||
gettingStated,
|
||||
layout: ViewLayoutPB.Document,
|
||||
onHover: () async {
|
||||
await tester.tapDeletePageButton();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'sidebar_test.dart' as sidebar_test;
|
||||
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
|
||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Sidebar integration tests
|
||||
sidebar_test.main();
|
||||
sidebar_expanded_test.main();
|
||||
sidebar_favorite_test.main();
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'dart:ui';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
@ -14,6 +13,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_langua
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -199,6 +199,18 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButtonWithName(ViewMoreActionType.rename.name);
|
||||
}
|
||||
|
||||
/// Tap the favorite page button
|
||||
Future<void> tapFavoritePageButton() async {
|
||||
await tapPageOptionButton();
|
||||
await tapButtonWithName(ViewMoreActionType.favorite.name);
|
||||
}
|
||||
|
||||
/// Tap the unfavorite page button
|
||||
Future<void> tapUnfavoritePageButton() async {
|
||||
await tapPageOptionButton();
|
||||
await tapButtonWithName(ViewMoreActionType.unFavorite.name);
|
||||
}
|
||||
|
||||
/// Rename the page.
|
||||
Future<void> renamePage(String name) async {
|
||||
await tapRenamePageButton();
|
||||
@ -332,6 +344,36 @@ extension CommonOperations on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> favoriteViewByName(
|
||||
String name, {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
}) async {
|
||||
await hoverOnPageName(
|
||||
name,
|
||||
layout: layout,
|
||||
useLast: false,
|
||||
onHover: () async {
|
||||
await tapFavoritePageButton();
|
||||
await pumpAndSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> unfavoriteViewByName(
|
||||
String name, {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
}) async {
|
||||
await hoverOnPageName(
|
||||
name,
|
||||
layout: layout,
|
||||
useLast: false,
|
||||
onHover: () async {
|
||||
await tapUnfavoritePageButton();
|
||||
await pumpAndSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> movePageToOtherPage({
|
||||
required String name,
|
||||
required String parentName,
|
||||
|
@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
@ -147,7 +148,24 @@ extension Expectation on WidgetTester {
|
||||
expect(textWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Find the page name on the home page.
|
||||
/// Find if the page is favorite
|
||||
Finder findFavoritePageName(
|
||||
String name, {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
String? parentName,
|
||||
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
|
||||
}) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite &&
|
||||
widget.view.name == name &&
|
||||
widget.view.layout == layout,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
|
||||
Finder findPageName(
|
||||
String name, {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
@ -168,11 +186,11 @@ extension Expectation on WidgetTester {
|
||||
of: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.name == name &&
|
||||
widget.view.layout == layout,
|
||||
widget.view.name == parentName &&
|
||||
widget.view.layout == parentLayout,
|
||||
skipOffstage: false,
|
||||
),
|
||||
matching: findPageName(name),
|
||||
matching: findPageName(name, layout: layout),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,10 @@ class KVKeys {
|
||||
/// The value is a json string with the following format:
|
||||
/// {'viewId': true, 'viewId2': false}
|
||||
static const String expandedViews = 'expandedViews';
|
||||
|
||||
/// The key for saving the expanded folder
|
||||
///
|
||||
/// The value is a json string with the following format:
|
||||
/// {'SidebarFolderCategoryType.value': true}
|
||||
static const String expandedFolders = 'expandedFolders';
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
||||
@ -155,11 +154,10 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case _ActionType.viewDatabase:
|
||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: viewPB.plugin(),
|
||||
view: viewPB,
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
@ -4,7 +4,6 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
@ -109,10 +108,10 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
|
||||
Log.error('Page($pageId) not found');
|
||||
return;
|
||||
}
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: view.plugin(),
|
||||
view: view,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
||||
@ -156,6 +157,7 @@ void _resolveFolderDeps(GetIt getIt) {
|
||||
getIt.registerFactory<TrashBloc>(
|
||||
() => TrashBloc(),
|
||||
);
|
||||
getIt.registerFactory<FavoriteBloc>(() => FavoriteBloc());
|
||||
}
|
||||
|
||||
void _resolveDocDeps(GetIt getIt) {
|
||||
|
@ -0,0 +1,93 @@
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'favorite_listener.dart';
|
||||
|
||||
part 'favorite_bloc.freezed.dart';
|
||||
|
||||
class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
||||
final _service = FavoriteService();
|
||||
final _listener = FavoriteListener();
|
||||
|
||||
FavoriteBloc() : super(FavoriteState.initial()) {
|
||||
on<FavoriteEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (e) async {
|
||||
_listener.start(
|
||||
favoritesUpdated: _onFavoritesUpdated,
|
||||
);
|
||||
final result = await _service.readFavorites();
|
||||
emit(
|
||||
result.fold(
|
||||
(view) => state.copyWith(
|
||||
views: view.items,
|
||||
),
|
||||
(error) => state.copyWith(
|
||||
views: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
didFavorite: (e) {
|
||||
emit(
|
||||
state.copyWith(views: [...state.views, ...e.favorite.items]),
|
||||
);
|
||||
},
|
||||
didUnfavorite: (e) {
|
||||
final views = [...state.views]..removeWhere(
|
||||
(view) => e.favorite.items.any((item) => item.id == view.id),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(views: views),
|
||||
);
|
||||
},
|
||||
toggle: (e) async {
|
||||
await _service.toggleFavorite(
|
||||
e.view.id,
|
||||
!e.view.isFavorite,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onFavoritesUpdated(
|
||||
Either<FlowyError, RepeatedViewPB> favoriteOrFailed,
|
||||
bool didFavorite,
|
||||
) {
|
||||
favoriteOrFailed.fold(
|
||||
(error) => Log.error(error),
|
||||
(favorite) => didFavorite
|
||||
? add(FavoriteEvent.didFavorite(favorite))
|
||||
: add(FavoriteEvent.didUnfavorite(favorite)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FavoriteEvent with _$FavoriteEvent {
|
||||
const factory FavoriteEvent.initial() = Initial;
|
||||
const factory FavoriteEvent.didFavorite(RepeatedViewPB favorite) =
|
||||
DidFavorite;
|
||||
const factory FavoriteEvent.didUnfavorite(RepeatedViewPB favorite) =
|
||||
DidUnfavorite;
|
||||
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FavoriteState with _$FavoriteState {
|
||||
const factory FavoriteState({
|
||||
required List<ViewPB> views,
|
||||
}) = _FavoriteState;
|
||||
|
||||
factory FavoriteState.initial() => const FavoriteState(
|
||||
views: [],
|
||||
);
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
||||
import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef FavoriteUpdated = void Function(
|
||||
Either<FlowyError, RepeatedViewPB> result,
|
||||
bool isFavorite,
|
||||
);
|
||||
|
||||
class FavoriteListener {
|
||||
StreamSubscription<SubscribeObject>? _streamSubscription;
|
||||
FolderNotificationParser? _parser;
|
||||
|
||||
FavoriteUpdated? _favoriteUpdated;
|
||||
|
||||
void start({
|
||||
FavoriteUpdated? favoritesUpdated,
|
||||
}) {
|
||||
_favoriteUpdated = favoritesUpdated;
|
||||
_parser = FolderNotificationParser(
|
||||
id: 'favorite',
|
||||
callback: _observableCallback,
|
||||
);
|
||||
_streamSubscription = RustStreamReceiver.listen(
|
||||
(observable) => _parser?.parse(observable),
|
||||
);
|
||||
}
|
||||
|
||||
void _observableCallback(
|
||||
FolderNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
if (_favoriteUpdated == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isFavorite = ty == FolderNotification.DidFavoriteView;
|
||||
result.fold(
|
||||
(payload) {
|
||||
final view = RepeatedViewPB.fromBuffer(payload);
|
||||
_favoriteUpdated!(
|
||||
right(view),
|
||||
isFavorite,
|
||||
);
|
||||
},
|
||||
(error) => _favoriteUpdated!(
|
||||
left(error),
|
||||
isFavorite,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_parser = null;
|
||||
await _streamSubscription?.cancel();
|
||||
_favoriteUpdated = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class FavoriteService {
|
||||
Future<Either<RepeatedViewPB, FlowyError>> readFavorites() {
|
||||
return FolderEventReadFavorites().send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> toggleFavorite(
|
||||
String viewId,
|
||||
bool favoriteStatus,
|
||||
) async {
|
||||
final id = RepeatedViewIdPB.create()..items.add(viewId);
|
||||
return FolderEventToggleFavorite(id).send();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export 'favorite_bloc.dart';
|
||||
export 'favorite_listener.dart';
|
||||
export 'favorite_service.dart';
|
@ -41,7 +41,8 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
|
||||
createApp: (_CreateApp event) async {
|
||||
final result = await _workspaceService.createApp(
|
||||
name: event.name,
|
||||
desc: event.desc ?? "",
|
||||
desc: event.desc,
|
||||
index: 0, // default to the first index
|
||||
);
|
||||
result.fold(
|
||||
(app) => emit(state.copyWith(plugin: app.plugin())),
|
||||
|
@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'folder_bloc.freezed.dart';
|
||||
|
||||
enum FolderCategoryType {
|
||||
favorite,
|
||||
personal,
|
||||
}
|
||||
|
||||
class FolderBloc extends Bloc<FolderEvent, FolderState> {
|
||||
FolderBloc({
|
||||
required FolderCategoryType type,
|
||||
}) : super(FolderState.initial(type)) {
|
||||
on<FolderEvent>((event, emit) async {
|
||||
await event.map(
|
||||
initial: (e) async {
|
||||
// fetch the expand status
|
||||
final isExpanded = await _getFolderExpandStatus();
|
||||
emit(state.copyWith(isExpanded: isExpanded));
|
||||
},
|
||||
expandOrUnExpand: (e) async {
|
||||
final isExpanded = e.isExpanded ?? !state.isExpanded;
|
||||
await _setFolderExpandStatus(e.isExpanded ?? !state.isExpanded);
|
||||
emit(state.copyWith(isExpanded: isExpanded));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _setFolderExpandStatus(bool isExpanded) async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
|
||||
final map = result.fold(
|
||||
(l) => {},
|
||||
(r) => jsonDecode(r),
|
||||
);
|
||||
if (isExpanded) {
|
||||
// set expand status to true if it's not expanded
|
||||
map[state.type.name] = true;
|
||||
} else {
|
||||
// remove the expand status if it's expanded
|
||||
map.remove(state.type.name);
|
||||
}
|
||||
await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));
|
||||
}
|
||||
|
||||
Future<bool> _getFolderExpandStatus() async {
|
||||
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
||||
return result.fold((l) => true, (r) {
|
||||
final map = jsonDecode(r);
|
||||
return map[state.type.name] ?? true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FolderEvent with _$FolderEvent {
|
||||
const factory FolderEvent.initial() = Initial;
|
||||
const factory FolderEvent.expandOrUnExpand({
|
||||
bool? isExpanded,
|
||||
}) = ExpandOrUnExpand;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FolderState with _$FolderState {
|
||||
const factory FolderState({
|
||||
required FolderCategoryType type,
|
||||
required bool isExpanded,
|
||||
}) = _FolderState;
|
||||
|
||||
factory FolderState.initial(
|
||||
FolderCategoryType type,
|
||||
) =>
|
||||
FolderState(
|
||||
type: type,
|
||||
isExpanded: true,
|
||||
);
|
||||
}
|
@ -34,6 +34,10 @@ class ViewBackendService {
|
||||
/// the database id. For example: "database_id": "xxx"
|
||||
///
|
||||
Map<String, String> ext = const {},
|
||||
|
||||
/// The [index] is the index of the view in the parent view.
|
||||
/// If the index is null, the view will be added to the end of the list.
|
||||
int? index,
|
||||
}) {
|
||||
final payload = CreateViewPayloadPB.create()
|
||||
..parentViewId = parentViewId
|
||||
@ -47,6 +51,14 @@ class ViewBackendService {
|
||||
payload.meta.addAll(ext);
|
||||
}
|
||||
|
||||
if (desc != null) {
|
||||
payload.desc = desc;
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
payload.index = index;
|
||||
}
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
||||
@ -118,11 +130,17 @@ class ViewBackendService {
|
||||
return FolderEventDuplicateView(view).send();
|
||||
}
|
||||
|
||||
static Future<Either<Unit, FlowyError>> favorite({required String viewId}) {
|
||||
final request = RepeatedViewIdPB.create()..items.add(viewId);
|
||||
return FolderEventToggleFavorite(request).send();
|
||||
}
|
||||
|
||||
static Future<Either<ViewPB, FlowyError>> updateView({
|
||||
required String viewId,
|
||||
String? name,
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
bool? isFavorite,
|
||||
}) {
|
||||
final payload = UpdateViewPayloadPB.create()..viewId = viewId;
|
||||
|
||||
@ -138,6 +156,9 @@ class ViewBackendService {
|
||||
payload.coverUrl = coverURL;
|
||||
}
|
||||
|
||||
if (isFavorite != null) {
|
||||
payload.isFavorite = isFavorite;
|
||||
}
|
||||
return FolderEventUpdateView(payload).send();
|
||||
}
|
||||
|
||||
|
@ -15,16 +15,25 @@ class WorkspaceService {
|
||||
WorkspaceService({
|
||||
required this.workspaceId,
|
||||
});
|
||||
|
||||
Future<Either<ViewPB, FlowyError>> createApp({
|
||||
required String name,
|
||||
String? desc,
|
||||
int? index,
|
||||
}) {
|
||||
final payload = CreateViewPayloadPB.create()
|
||||
..parentViewId = workspaceId
|
||||
..name = name
|
||||
..desc = desc ?? ""
|
||||
..layout = ViewLayoutPB.Document;
|
||||
|
||||
if (desc != null) {
|
||||
payload.desc = desc;
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
payload.index = index;
|
||||
}
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@ -92,6 +93,7 @@ class ViewSectionItem extends StatelessWidget {
|
||||
if (onHover || state.isEditing) {
|
||||
children.add(
|
||||
ViewDisclosureButton(
|
||||
state: state,
|
||||
onEdit: (isEdit) =>
|
||||
blocContext.read<ViewBloc>().add(ViewEvent.setIsEditing(isEdit)),
|
||||
onAction: (action) {
|
||||
@ -115,6 +117,11 @@ class ViewSectionItem extends StatelessWidget {
|
||||
case ViewDisclosureAction.duplicate:
|
||||
blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
break;
|
||||
case ViewDisclosureAction.favorite:
|
||||
blocContext
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(view));
|
||||
break;
|
||||
case ViewDisclosureAction.openInNewTab:
|
||||
blocContext.read<TabsBloc>().add(
|
||||
TabsEvent.openTab(
|
||||
@ -143,11 +150,12 @@ enum ViewDisclosureAction {
|
||||
rename,
|
||||
delete,
|
||||
duplicate,
|
||||
favorite,
|
||||
openInNewTab,
|
||||
}
|
||||
|
||||
extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
String get name {
|
||||
String name({ViewState? state}) {
|
||||
switch (this) {
|
||||
case ViewDisclosureAction.rename:
|
||||
return LocaleKeys.disclosureAction_rename.tr();
|
||||
@ -155,12 +163,16 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
return LocaleKeys.disclosureAction_delete.tr();
|
||||
case ViewDisclosureAction.duplicate:
|
||||
return LocaleKeys.disclosureAction_duplicate.tr();
|
||||
case ViewDisclosureAction.favorite:
|
||||
return state!.view.isFavorite
|
||||
? LocaleKeys.disclosureAction_unfavorite.tr()
|
||||
: LocaleKeys.disclosureAction_favorite.tr();
|
||||
case ViewDisclosureAction.openInNewTab:
|
||||
return LocaleKeys.disclosureAction_openNewTab.tr();
|
||||
}
|
||||
}
|
||||
|
||||
Widget icon(Color iconColor) {
|
||||
Widget icon(Color iconColor, {ViewState? state}) {
|
||||
switch (this) {
|
||||
case ViewDisclosureAction.rename:
|
||||
return const FlowySvg(name: 'editor/edit');
|
||||
@ -168,6 +180,10 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
return const FlowySvg(name: 'editor/delete');
|
||||
case ViewDisclosureAction.duplicate:
|
||||
return const FlowySvg(name: 'editor/copy');
|
||||
case ViewDisclosureAction.favorite:
|
||||
return state!.view.isFavorite
|
||||
? const FlowySvg(name: 'home/favorite')
|
||||
: const FlowySvg(name: 'home/unfavorite');
|
||||
case ViewDisclosureAction.openInNewTab:
|
||||
return const FlowySvg(name: 'grid/expander');
|
||||
}
|
||||
@ -177,9 +193,11 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
class ViewDisclosureButton extends StatelessWidget {
|
||||
final Function(bool) onEdit;
|
||||
final Function(ViewDisclosureAction) onAction;
|
||||
final ViewState state;
|
||||
const ViewDisclosureButton({
|
||||
required this.onEdit,
|
||||
required this.onAction,
|
||||
required this.state,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -188,7 +206,7 @@ class ViewDisclosureButton extends StatelessWidget {
|
||||
return PopoverActionList<ViewDisclosureActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: ViewDisclosureAction.values
|
||||
.map((action) => ViewDisclosureActionWrapper(action))
|
||||
.map((action) => ViewDisclosureActionWrapper(action, state))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return FlowyIconButton(
|
||||
@ -219,11 +237,12 @@ class ViewDisclosureButton extends StatelessWidget {
|
||||
|
||||
class ViewDisclosureActionWrapper extends ActionCell {
|
||||
final ViewDisclosureAction inner;
|
||||
final ViewState? state;
|
||||
|
||||
ViewDisclosureActionWrapper(this.inner);
|
||||
ViewDisclosureActionWrapper(this.inner, [this.state]);
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
|
||||
Widget? leftIcon(Color iconColor) => inner.icon(iconColor, state: state);
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
String get name => inner.name(state: state);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -1,10 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FavoriteHeader extends StatelessWidget {
|
||||
const FavoriteHeader({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/core/frameless_window.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/trash/menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
@ -27,6 +28,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import '../navigation.dart';
|
||||
import 'app/create_button.dart';
|
||||
import 'app/menu_app.dart';
|
||||
import 'app/section/item.dart';
|
||||
import 'menu_user.dart';
|
||||
|
||||
export './app/header/header.dart';
|
||||
@ -56,6 +58,10 @@ class HomeMenu extends StatelessWidget {
|
||||
return menuBloc;
|
||||
},
|
||||
),
|
||||
BlocProvider(
|
||||
create: (ctx) =>
|
||||
getIt<FavoriteBloc>()..add(const FavoriteEvent.initial()),
|
||||
)
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
@ -105,6 +111,50 @@ class HomeMenu extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderFavorites(BuildContext context) {
|
||||
return BlocBuilder<FavoriteBloc, FavoriteState>(
|
||||
builder: (context, state) {
|
||||
return state.views.isNotEmpty
|
||||
? ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
useInkWell: true,
|
||||
animationDuration: Durations.medium,
|
||||
),
|
||||
child: ExpandablePanel(
|
||||
theme: const ExpandableThemeData(
|
||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||
tapBodyToExpand: false,
|
||||
tapBodyToCollapse: false,
|
||||
tapHeaderToExpand: false,
|
||||
iconPadding: EdgeInsets.zero,
|
||||
hasIcon: false,
|
||||
),
|
||||
// header: const FavoriteHeader(),
|
||||
expanded: ScrollConfiguration(
|
||||
behavior:
|
||||
const ScrollBehavior().copyWith(scrollbars: false),
|
||||
child: Column(
|
||||
children: state.views
|
||||
.map(
|
||||
(e) => ViewSectionItem(
|
||||
key: ValueKey(e.id),
|
||||
isSelected: false,
|
||||
onSelected: (view) => getIt<MenuSharedState>()
|
||||
.latestOpenView = view,
|
||||
view: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
collapsed: const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderApps(BuildContext context) {
|
||||
return ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
@ -122,10 +172,16 @@ class HomeMenu extends StatelessWidget {
|
||||
return ReorderableListView.builder(
|
||||
itemCount: menuItems.length,
|
||||
buildDefaultDragHandles: false,
|
||||
header: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
|
||||
child: MenuUser(user),
|
||||
header: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MenuAppSizes.appVPadding,
|
||||
),
|
||||
child: MenuUser(user),
|
||||
),
|
||||
_renderFavorites(context),
|
||||
],
|
||||
),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
// Moving item1 from index 0 to index 1
|
||||
@ -180,7 +236,7 @@ class MenuSharedState {
|
||||
ValueNotifier<ViewPB?> get notifier => _latestOpenView;
|
||||
|
||||
set latestOpenView(ViewPB? view) {
|
||||
if (_latestOpenView.value != view) {
|
||||
if (_latestOpenView.value?.id != view?.id) {
|
||||
_latestOpenView.value = view;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,109 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class FavoriteFolder extends StatelessWidget {
|
||||
const FavoriteFolder({
|
||||
super.key,
|
||||
required this.views,
|
||||
});
|
||||
|
||||
final List<ViewPB> views;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (views.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return BlocProvider<FolderBloc>(
|
||||
create: (context) => FolderBloc(type: FolderCategoryType.favorite)
|
||||
..add(
|
||||
const FolderEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<FolderBloc, FolderState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
FavoriteHeader(
|
||||
onPressed: () => context
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand()),
|
||||
onAdded: () => context
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand(isExpanded: true)),
|
||||
),
|
||||
if (state.isExpanded)
|
||||
...views.map(
|
||||
(view) => ViewItem(
|
||||
key: ValueKey(
|
||||
'${FolderCategoryType.favorite.name} ${view.id}',
|
||||
),
|
||||
categoryType: FolderCategoryType.favorite,
|
||||
isDraggable: false,
|
||||
isFirstChild: view.id == views.first.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
onSelected: (view) {
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
context
|
||||
.read<MenuBloc>()
|
||||
.add(MenuEvent.openPage(view.plugin()));
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FavoriteHeader extends StatefulWidget {
|
||||
const FavoriteHeader({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.onAdded,
|
||||
});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback onAdded;
|
||||
|
||||
@override
|
||||
State<FavoriteHeader> createState() => _FavoriteHeaderState();
|
||||
}
|
||||
|
||||
class _FavoriteHeaderState extends State<FavoriteHeader> {
|
||||
bool onHover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const iconSize = 26.0;
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setState(() => onHover = true),
|
||||
onExit: (event) => setState(() => onHover = false),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FlowyTextButton(
|
||||
LocaleKeys.sideBar_favorites.tr(),
|
||||
tooltip: LocaleKeys.sideBar_clickToHideFavorites.tr(),
|
||||
constraints: const BoxConstraints(maxHeight: iconSize),
|
||||
padding: const EdgeInsets.all(4),
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: widget.onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_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_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -11,7 +12,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PersonalFolder extends StatefulWidget {
|
||||
class PersonalFolder extends StatelessWidget {
|
||||
const PersonalFolder({
|
||||
super.key,
|
||||
required this.views,
|
||||
@ -19,37 +20,49 @@ class PersonalFolder extends StatefulWidget {
|
||||
|
||||
final List<ViewPB> views;
|
||||
|
||||
@override
|
||||
State<PersonalFolder> createState() => _PersonalFolderState();
|
||||
}
|
||||
|
||||
class _PersonalFolderState extends State<PersonalFolder> {
|
||||
bool isExpanded = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
PersonalFolderHeader(
|
||||
onPressed: () => setState(
|
||||
() => isExpanded = !isExpanded,
|
||||
),
|
||||
onAdded: () => setState(() => isExpanded = true),
|
||||
return BlocProvider<FolderBloc>(
|
||||
create: (context) => FolderBloc(type: FolderCategoryType.personal)
|
||||
..add(
|
||||
const FolderEvent.initial(),
|
||||
),
|
||||
if (isExpanded)
|
||||
...widget.views.map(
|
||||
(view) => ViewItem(
|
||||
key: ValueKey(view.id),
|
||||
isFirstChild: view.id == widget.views.first.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
onSelected: (view) {
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
context.read<MenuBloc>().add(MenuEvent.openPage(view.plugin()));
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
child: BlocBuilder<FolderBloc, FolderState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
PersonalFolderHeader(
|
||||
onPressed: () => context
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand()),
|
||||
onAdded: () => context
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand(isExpanded: true)),
|
||||
),
|
||||
if (state.isExpanded)
|
||||
...views.map(
|
||||
(view) => ViewItem(
|
||||
key: ValueKey(
|
||||
'${FolderCategoryType.personal.name} ${view.id}',
|
||||
),
|
||||
categoryType: FolderCategoryType.personal,
|
||||
isFirstChild: view.id == views.first.id,
|
||||
view: view,
|
||||
level: 0,
|
||||
onSelected: (view) {
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: view.plugin(),
|
||||
view: view,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
||||
@ -33,22 +34,43 @@ class HomeSideBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => MenuBloc(
|
||||
user: user,
|
||||
workspace: workspaceSetting.workspace,
|
||||
)..add(const MenuEvent.initial()),
|
||||
child: BlocConsumer<MenuBloc, MenuState>(
|
||||
builder: (context, state) => _buildSidebar(context, state),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => MenuBloc(
|
||||
user: user,
|
||||
workspace: workspaceSetting.workspace,
|
||||
)..add(const MenuEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
)
|
||||
],
|
||||
child: BlocListener<MenuBloc, MenuState>(
|
||||
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
|
||||
listener: (context, state) => getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(plugin: state.plugin),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final menuState = context.watch<MenuBloc>().state;
|
||||
final favoriteState = context.watch<FavoriteBloc>().state;
|
||||
return _buildSidebar(
|
||||
context,
|
||||
menuState,
|
||||
favoriteState,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSidebar(BuildContext context, MenuState state) {
|
||||
Widget _buildSidebar(
|
||||
BuildContext context,
|
||||
MenuState state,
|
||||
FavoriteState favoriteState,
|
||||
) {
|
||||
final views = state.views;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@ -67,13 +89,13 @@ class HomeSideBar extends StatelessWidget {
|
||||
const SidebarTopMenu(),
|
||||
// user, setting
|
||||
SidebarUser(user: user),
|
||||
// Favorite, Not supported yet
|
||||
const VSpace(20),
|
||||
// scrollable document list
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SidebarFolder(
|
||||
views: views,
|
||||
favoriteViews: favoriteState.views,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,23 +1,40 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SidebarFolder extends StatelessWidget {
|
||||
const SidebarFolder({
|
||||
super.key,
|
||||
required this.views,
|
||||
required this.favoriteViews,
|
||||
});
|
||||
|
||||
final List<ViewPB> views;
|
||||
final List<ViewPB> favoriteViews;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
// personal
|
||||
PersonalFolder(views: views),
|
||||
],
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: getIt<MenuSharedState>().notifier,
|
||||
builder: (context, value, child) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
// favorite
|
||||
if (favoriteViews.isNotEmpty)
|
||||
FavoriteFolder(
|
||||
views: favoriteViews,
|
||||
),
|
||||
const VSpace(10),
|
||||
// personal
|
||||
PersonalFolder(views: views),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import 'package:flutter/material.dart';
|
||||
|
||||
enum ViewMoreActionType {
|
||||
delete,
|
||||
addToFavorites, // not supported yet.
|
||||
favorite,
|
||||
unFavorite,
|
||||
duplicate,
|
||||
copyLink, // not supported yet.
|
||||
rename,
|
||||
@ -18,8 +19,10 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
switch (this) {
|
||||
case ViewMoreActionType.delete:
|
||||
return LocaleKeys.disclosureAction_delete.tr();
|
||||
case ViewMoreActionType.addToFavorites:
|
||||
return LocaleKeys.disclosureAction_addToFavorites.tr();
|
||||
case ViewMoreActionType.favorite:
|
||||
return LocaleKeys.disclosureAction_favorite.tr();
|
||||
case ViewMoreActionType.unFavorite:
|
||||
return LocaleKeys.disclosureAction_unfavorite.tr();
|
||||
case ViewMoreActionType.duplicate:
|
||||
return LocaleKeys.disclosureAction_duplicate.tr();
|
||||
case ViewMoreActionType.copyLink:
|
||||
@ -37,8 +40,10 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
switch (this) {
|
||||
case ViewMoreActionType.delete:
|
||||
return const FlowySvg(name: 'editor/delete');
|
||||
case ViewMoreActionType.addToFavorites:
|
||||
return const Icon(Icons.favorite);
|
||||
case ViewMoreActionType.favorite:
|
||||
return const FlowySvg(name: 'home/unfavorite');
|
||||
case ViewMoreActionType.unFavorite:
|
||||
return const FlowySvg(name: 'home/favorite');
|
||||
case ViewMoreActionType.duplicate:
|
||||
return const FlowySvg(name: 'editor/copy');
|
||||
case ViewMoreActionType.copyLink:
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.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/application/view/view_ext.dart';
|
||||
@ -21,6 +23,7 @@ class ViewItem extends StatelessWidget {
|
||||
const ViewItem({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.categoryType,
|
||||
required this.level,
|
||||
this.leftPadding = 10,
|
||||
required this.onSelected,
|
||||
@ -30,6 +33,8 @@ class ViewItem extends StatelessWidget {
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
final FolderCategoryType categoryType;
|
||||
|
||||
// indicate the level of the view item
|
||||
// used to calculate the left padding
|
||||
final int level;
|
||||
@ -53,11 +58,10 @@ class ViewItem extends StatelessWidget {
|
||||
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, state) {
|
||||
view.childViews
|
||||
..clear()
|
||||
..addAll(state.childViews);
|
||||
return InnerViewItem(
|
||||
view: view,
|
||||
view: state.view,
|
||||
childViews: state.childViews,
|
||||
categoryType: categoryType,
|
||||
level: level,
|
||||
leftPadding: leftPadding,
|
||||
showActions: state.isEditing,
|
||||
@ -76,6 +80,8 @@ class InnerViewItem extends StatelessWidget {
|
||||
const InnerViewItem({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.childViews,
|
||||
required this.categoryType,
|
||||
this.isDraggable = true,
|
||||
this.isExpanded = true,
|
||||
required this.level,
|
||||
@ -86,6 +92,8 @@ class InnerViewItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final List<ViewPB> childViews;
|
||||
final FolderCategoryType categoryType;
|
||||
|
||||
final bool isDraggable;
|
||||
final bool isExpanded;
|
||||
@ -108,11 +116,11 @@ class InnerViewItem extends StatelessWidget {
|
||||
);
|
||||
|
||||
// if the view is expanded and has child views, render its child views
|
||||
final childViews = view.childViews;
|
||||
if (isExpanded && childViews.isNotEmpty) {
|
||||
final children = childViews.map((childView) {
|
||||
return ViewItem(
|
||||
key: ValueKey(childView.id),
|
||||
key: ValueKey('${categoryType.name} ${childView.id}'),
|
||||
categoryType: categoryType,
|
||||
isFirstChild: childView.id == childViews.first.id,
|
||||
view: childView,
|
||||
level: level + 1,
|
||||
@ -139,12 +147,19 @@ class InnerViewItem extends StatelessWidget {
|
||||
feedback: (context) {
|
||||
return ViewItem(
|
||||
view: view,
|
||||
categoryType: categoryType,
|
||||
level: level,
|
||||
onSelected: onSelected,
|
||||
isDraggable: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// keep the same height of the DraggableItem
|
||||
child = Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
@ -218,7 +233,8 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
children.add(_buildViewAddButton(context));
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
// Don't use GestureDetector here, because it doesn't response to the tap event sometimes.
|
||||
return InkWell(
|
||||
onTap: () => widget.onSelected(widget.view),
|
||||
child: SizedBox(
|
||||
height: 26,
|
||||
@ -284,10 +300,17 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
return Tooltip(
|
||||
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
||||
child: ViewMoreActionButton(
|
||||
view: widget.view,
|
||||
onEditing: (value) =>
|
||||
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case ViewMoreActionType.favorite:
|
||||
case ViewMoreActionType.unFavorite:
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
break;
|
||||
case ViewMoreActionType.rename:
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
|
||||
@ -6,26 +7,30 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
||||
const supportedActionTypes = [
|
||||
ViewMoreActionType.rename,
|
||||
ViewMoreActionType.delete,
|
||||
ViewMoreActionType.duplicate,
|
||||
ViewMoreActionType.openInNewTab,
|
||||
];
|
||||
|
||||
/// ··· button beside the view name
|
||||
class ViewMoreActionButton extends StatelessWidget {
|
||||
const ViewMoreActionButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.onEditing,
|
||||
required this.onAction,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final void Function(bool value) onEditing;
|
||||
final void Function(ViewMoreActionType) onAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final supportedActionTypes = [
|
||||
ViewMoreActionType.rename,
|
||||
ViewMoreActionType.delete,
|
||||
ViewMoreActionType.duplicate,
|
||||
ViewMoreActionType.openInNewTab,
|
||||
view.isFavorite
|
||||
? ViewMoreActionType.unFavorite
|
||||
: ViewMoreActionType.favorite,
|
||||
];
|
||||
return PopoverActionList<ViewMoreActionTypeWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 8),
|
||||
|
@ -32,11 +32,8 @@ void main() {
|
||||
menuBloc.add(const MenuEvent.createApp("App 3"));
|
||||
await blocResponseFuture();
|
||||
|
||||
menuBloc.add(const MenuEvent.moveApp(1, 3));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(menuBloc.state.views[0].name == 'App 3');
|
||||
assert(menuBloc.state.views[1].name == 'App 2');
|
||||
assert(menuBloc.state.views[2].name == 'App 3');
|
||||
assert(menuBloc.state.views[3].name == 'App 1');
|
||||
assert(menuBloc.state.views[2].name == 'App 1');
|
||||
});
|
||||
}
|
||||
|
@ -34,13 +34,12 @@ default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[patch.crates-io]
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
appflowy-integrate = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-database = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-document = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-folder = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-plugins = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
|
||||
#collab = { path = "../../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||
|
@ -70,6 +70,8 @@
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"duplicate": "Duplicate",
|
||||
"unfavorite": "Remove from favorites",
|
||||
"favorite": "Add to favorites",
|
||||
"openNewTab": "Open in a new tab",
|
||||
"moveTo": "Move to",
|
||||
"addToFavorites": "Add to Favorites",
|
||||
@ -154,6 +156,7 @@
|
||||
"personal": "Personal",
|
||||
"favorites": "Favorites",
|
||||
"clickToHidePersonal": "Click to hide personal section",
|
||||
"clickToHideFavorites": "Click to hide favorite section",
|
||||
"addAPage": "Add a page"
|
||||
},
|
||||
"notifications": {
|
||||
|
20
frontend/rust-lib/Cargo.lock
generated
20
frontend/rust-lib/Cargo.lock
generated
@ -85,7 +85,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -888,7 +888,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -906,7 +906,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -924,7 +924,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -951,7 +951,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -963,7 +963,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -982,7 +982,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -1002,7 +1002,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -1022,7 +1022,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1052,7 +1052,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5783a5#5783a5ba6416125b669814d4089ed9afaf3469b5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f9df5b9#f9df5b9b5bf1e74c305aafcaf57b7b18493bded5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
|
@ -38,12 +38,12 @@ opt-level = 3
|
||||
incremental = false
|
||||
|
||||
[patch.crates-io]
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5783a5" }
|
||||
appflowy-integrate = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-database = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-document = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-folder = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
collab-plugins = {git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f9df5b9"}
|
||||
|
||||
#collab = { path = "../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
||||
@ -51,4 +51,3 @@ collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev =
|
||||
#collab-document = { path = "../AppFlowy-Collab/collab-document" }
|
||||
#collab-plugins = { path = "../AppFlowy-Collab/collab-plugins" }
|
||||
#appflowy-integrate = { path = "../AppFlowy-Collab/appflowy-integrate" }
|
||||
|
||||
|
@ -58,6 +58,9 @@ pub struct ViewPB {
|
||||
/// The cover url of the view.
|
||||
#[pb(index = 8, one_of)]
|
||||
pub cover_url: Option<String>,
|
||||
|
||||
#[pb(index = 9)]
|
||||
pub is_favorite: bool,
|
||||
}
|
||||
|
||||
pub fn view_pb_without_child_views(view: Arc<View>) -> ViewPB {
|
||||
@ -70,6 +73,7 @@ pub fn view_pb_without_child_views(view: Arc<View>) -> ViewPB {
|
||||
layout: view.layout.clone().into(),
|
||||
icon_url: view.icon_url.clone(),
|
||||
cover_url: view.cover_url.clone(),
|
||||
is_favorite: view.is_favorite.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +91,7 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) ->
|
||||
layout: view.layout.clone().into(),
|
||||
icon_url: view.icon_url.clone(),
|
||||
cover_url: view.cover_url.clone(),
|
||||
is_favorite: view.is_favorite.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,9 +179,14 @@ pub struct CreateViewPayloadPB {
|
||||
#[pb(index = 7)]
|
||||
pub meta: HashMap<String, String>,
|
||||
|
||||
/// Mark the view as current view after creation.
|
||||
// Mark the view as current view after creation.
|
||||
#[pb(index = 8)]
|
||||
pub set_as_current: bool,
|
||||
|
||||
// The index of the view in the parent view.
|
||||
// If the index is None or the index is out of range, the view will be appended to the end of the parent view.
|
||||
#[pb(index = 9, one_of)]
|
||||
pub index: Option<u32>,
|
||||
}
|
||||
|
||||
/// The orphan view is meant to be a view that is not attached to any parent view. By default, this
|
||||
@ -209,8 +219,11 @@ pub struct CreateViewParams {
|
||||
pub view_id: String,
|
||||
pub initial_data: Vec<u8>,
|
||||
pub meta: HashMap<String, String>,
|
||||
/// Mark the view as current view after creation.
|
||||
// Mark the view as current view after creation.
|
||||
pub set_as_current: bool,
|
||||
// The index of the view in the parent view.
|
||||
// If the index is None or the index is out of range, the view will be appended to the end of the parent view.
|
||||
pub index: Option<u32>,
|
||||
}
|
||||
|
||||
impl TryInto<CreateViewParams> for CreateViewPayloadPB {
|
||||
@ -230,6 +243,7 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB {
|
||||
initial_data: self.initial_data,
|
||||
meta: self.meta,
|
||||
set_as_current: self.set_as_current,
|
||||
index: self.index,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -250,6 +264,7 @@ impl TryInto<CreateViewParams> for CreateOrphanViewPayloadPB {
|
||||
initial_data: self.initial_data,
|
||||
meta: Default::default(),
|
||||
set_as_current: false,
|
||||
index: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -307,6 +322,9 @@ pub struct UpdateViewPayloadPB {
|
||||
|
||||
#[pb(index = 7, one_of)]
|
||||
pub cover_url: Option<String>,
|
||||
|
||||
#[pb(index = 8, one_of)]
|
||||
pub is_favorite: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -315,13 +333,10 @@ pub struct UpdateViewParams {
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub layout: Option<ViewLayout>,
|
||||
|
||||
/// The icon url can be empty, which means the view has no icon.
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
/// The cover url can be empty, which means the view has no icon.
|
||||
pub cover_url: Option<String>,
|
||||
pub is_favorite: Option<bool>,
|
||||
pub layout: Option<ViewLayout>,
|
||||
}
|
||||
|
||||
impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
|
||||
@ -345,14 +360,19 @@ impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
|
||||
Some(thumbnail) => Some(ViewThumbnail::parse(thumbnail)?.0),
|
||||
};
|
||||
|
||||
let cover_url = self.cover_url;
|
||||
let icon_url = self.icon_url;
|
||||
let is_favorite = self.is_favorite;
|
||||
|
||||
Ok(UpdateViewParams {
|
||||
view_id,
|
||||
name,
|
||||
desc,
|
||||
thumbnail,
|
||||
cover_url,
|
||||
icon_url,
|
||||
is_favorite,
|
||||
layout: self.layout.map(|ty| ty.into()),
|
||||
icon_url: self.icon_url,
|
||||
cover_url: self.cover_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||
|
||||
use crate::entities::*;
|
||||
use crate::manager::FolderManager;
|
||||
use crate::share::ImportParams;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||
|
||||
fn upgrade_folder(
|
||||
folder_manager: AFPluginState<Weak<FolderManager>>,
|
||||
@ -152,6 +151,18 @@ pub(crate) async fn delete_view_handler(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_favorites_handler(
|
||||
data: AFPluginData<RepeatedViewIdPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: RepeatedViewIdPB = data.into_inner();
|
||||
let folder = upgrade_folder(folder)?;
|
||||
for view_id in ¶ms.items {
|
||||
let _ = folder.toggle_favorites(view_id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_latest_view_handler(
|
||||
data: AFPluginData<ViewIdPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
@ -208,6 +219,27 @@ pub(crate) async fn duplicate_view_handler(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(folder), err)]
|
||||
pub(crate) async fn read_favorites_handler(
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
) -> DataResult<RepeatedViewPB, FlowyError> {
|
||||
let folder = upgrade_folder(folder)?;
|
||||
let favorites = folder.get_all_favorites().await;
|
||||
let mut views = vec![];
|
||||
for info in favorites {
|
||||
let view = folder.get_view(&info.id).await;
|
||||
match view {
|
||||
Ok(view) => {
|
||||
views.push(view);
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
data_result_ok(RepeatedViewPB { items: views })
|
||||
}
|
||||
#[tracing::instrument(level = "debug", skip(folder), err)]
|
||||
pub(crate) async fn read_trash_handler(
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
|
@ -38,6 +38,8 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
|
||||
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler)
|
||||
.event(FolderEvent::ImportData, import_data_handler)
|
||||
.event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler)
|
||||
.event(FolderEvent::ReadFavorites, read_favorites_handler)
|
||||
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
@ -133,7 +135,6 @@ pub enum FolderEvent {
|
||||
|
||||
#[event()]
|
||||
GetFolderSnapshots = 31,
|
||||
|
||||
/// Moves a nested view to a new location in the hierarchy.
|
||||
///
|
||||
/// This function takes the `view_id` of the view to be moved,
|
||||
@ -142,4 +143,10 @@ pub enum FolderEvent {
|
||||
/// this specific view.
|
||||
#[event(input = "MoveNestedViewPayloadPB")]
|
||||
MoveNestedView = 32,
|
||||
|
||||
#[event(output = "RepeatedViewPB")]
|
||||
ReadFavorites = 33,
|
||||
|
||||
#[event(input = "RepeatedViewIdPB")]
|
||||
ToggleFavorite = 34,
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ use appflowy_integrate::{CollabPersistenceConfig, CollabType, RocksCollabDB};
|
||||
use collab::core::collab::{CollabRawData, MutexCollab};
|
||||
use collab::core::collab_state::SyncState;
|
||||
use collab_folder::core::{
|
||||
Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, View, ViewChange,
|
||||
ViewChangeReceiver, ViewLayout, Workspace,
|
||||
FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo,
|
||||
View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
@ -370,9 +370,10 @@ impl FolderManager {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let index = params.index;
|
||||
let view = create_view(params, view_layout);
|
||||
self.with_folder((), |folder| {
|
||||
folder.insert_view(view.clone());
|
||||
folder.insert_view(view.clone(), index);
|
||||
});
|
||||
|
||||
Ok(view)
|
||||
@ -393,7 +394,7 @@ impl FolderManager {
|
||||
.await?;
|
||||
let view = create_view(params, view_layout);
|
||||
self.with_folder((), |folder| {
|
||||
folder.insert_view(view.clone());
|
||||
folder.insert_view(view.clone(), None);
|
||||
});
|
||||
Ok(view)
|
||||
}
|
||||
@ -442,21 +443,21 @@ impl FolderManager {
|
||||
|
||||
/// Move the view to trash. If the view is the current view, then set the current view to empty.
|
||||
/// When the view is moved to trash, all the child views will be moved to trash as well.
|
||||
/// All the favorite views being trashed will be unfavorited first to remove it from favorites list as well. The process of unfavoriting concerned view is handled by `unfavorite_view_and_decendants()`
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> {
|
||||
self.with_folder((), |folder| {
|
||||
let view = folder.views.get_view(view_id);
|
||||
folder.add_trash(vec![view_id.to_string()]);
|
||||
if let Some(view) = folder.views.get_view(view_id) {
|
||||
self.unfavorite_view_and_decendants(view.clone(), &folder);
|
||||
folder.add_trash(vec![view_id.to_string()]);
|
||||
// notify the parent view that the view is moved to trash
|
||||
send_notification(view_id, FolderNotification::DidMoveViewToTrash)
|
||||
.payload(DeletedViewPB {
|
||||
view_id: view_id.to_string(),
|
||||
index: None,
|
||||
})
|
||||
.send();
|
||||
|
||||
// notify the parent view that the view is moved to trash
|
||||
send_notification(view_id, FolderNotification::DidMoveViewToTrash)
|
||||
.payload(DeletedViewPB {
|
||||
view_id: view_id.to_string(),
|
||||
index: None,
|
||||
})
|
||||
.send();
|
||||
|
||||
if let Some(view) = view {
|
||||
notify_child_views_changed(
|
||||
view_pb_without_child_views(view),
|
||||
ChildViewChangeReason::DidDeleteView,
|
||||
@ -467,6 +468,31 @@ impl FolderManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unfavorite_view_and_decendants(&self, view: Arc<View>, folder: &Folder) {
|
||||
let mut all_descendant_views: Vec<Arc<View>> = vec![view.clone()];
|
||||
all_descendant_views.extend(folder.views.get_views_belong_to(&view.id));
|
||||
|
||||
let favorite_descendant_views: Vec<ViewPB> = all_descendant_views
|
||||
.iter()
|
||||
.filter(|view| view.is_favorite)
|
||||
.map(|view| view_pb_without_child_views(view.clone()))
|
||||
.collect();
|
||||
|
||||
if !favorite_descendant_views.is_empty() {
|
||||
folder.delete_favorites(
|
||||
favorite_descendant_views
|
||||
.iter()
|
||||
.map(|v| v.id.clone())
|
||||
.collect(),
|
||||
);
|
||||
send_notification("favorite", FolderNotification::DidUnfavoriteView)
|
||||
.payload(RepeatedViewPB {
|
||||
items: favorite_descendant_views,
|
||||
})
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a nested view to a new location in the hierarchy.
|
||||
///
|
||||
/// This function takes the `view_id` of the view to be moved,
|
||||
@ -570,6 +596,7 @@ impl FolderManager {
|
||||
.set_layout_if_not_none(params.layout)
|
||||
.set_icon_url_if_not_none(params.icon_url)
|
||||
.set_cover_url_if_not_none(params.cover_url)
|
||||
.set_favorite_if_not_none(params.is_favorite)
|
||||
.done()
|
||||
});
|
||||
|
||||
@ -599,6 +626,14 @@ impl FolderManager {
|
||||
|
||||
let handler = self.get_handler(&view.layout)?;
|
||||
let view_data = handler.duplicate_view(&view.id).await?;
|
||||
|
||||
// get the current view index in the parent view, because we need to insert the duplicated view below the current view.
|
||||
let index = if let Some((_, __, views)) = self.get_view_relation(&view.parent_view_id).await {
|
||||
views.iter().position(|id| id == view_id).map(|i| i as u32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let duplicate_params = CreateViewParams {
|
||||
parent_view_id: view.parent_view_id.clone(),
|
||||
name: format!("{} (copy)", &view.name),
|
||||
@ -608,9 +643,10 @@ impl FolderManager {
|
||||
view_id: gen_view_id(),
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index,
|
||||
};
|
||||
|
||||
let _ = self.create_view_with_params(duplicate_params).await?;
|
||||
self.create_view_with_params(duplicate_params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -634,6 +670,57 @@ impl FolderManager {
|
||||
self.get_view(&view_id).await.ok()
|
||||
}
|
||||
|
||||
/// Toggles the favorite status of a view identified by `view_id`If the view is not a favorite, it will be added to the favorites list; otherwise, it will be removed from the list.
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> {
|
||||
self.with_folder((), |folder| {
|
||||
if let Some(old_view) = folder.views.get_view(view_id) {
|
||||
if old_view.is_favorite {
|
||||
folder.delete_favorites(vec![view_id.to_string()]);
|
||||
} else {
|
||||
folder.add_favorites(vec![view_id.to_string()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.send_toggle_favorite_notification(view_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed.
|
||||
async fn send_toggle_favorite_notification(&self, view_id: &str) {
|
||||
if let Ok(view) = self.get_view(view_id).await {
|
||||
let notification_type = if view.is_favorite {
|
||||
FolderNotification::DidFavoriteView
|
||||
} else {
|
||||
FolderNotification::DidUnfavoriteView
|
||||
};
|
||||
send_notification("favorite", notification_type)
|
||||
.payload(RepeatedViewPB {
|
||||
items: vec![view.clone()],
|
||||
})
|
||||
.send();
|
||||
|
||||
send_notification(&view.id, FolderNotification::DidUpdateView)
|
||||
.payload(view)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub(crate) async fn get_all_favorites(&self) -> Vec<FavoritesInfo> {
|
||||
self.with_folder(vec![], |folder| {
|
||||
let trash_ids = folder
|
||||
.get_all_trash()
|
||||
.into_iter()
|
||||
.map(|trash| trash.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut views = folder.get_all_favorites();
|
||||
views.retain(|view| !trash_ids.contains(&view.id));
|
||||
views
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub(crate) async fn get_all_trash(&self) -> Vec<TrashInfo> {
|
||||
self.with_folder(vec![], |folder| folder.get_all_trash())
|
||||
@ -644,7 +731,6 @@ impl FolderManager {
|
||||
self.with_folder((), |folder| {
|
||||
folder.remote_all_trash();
|
||||
});
|
||||
|
||||
send_notification("trash", FolderNotification::DidUpdateTrash)
|
||||
.payload(RepeatedTrashPB { items: vec![] })
|
||||
.send();
|
||||
@ -718,11 +804,12 @@ impl FolderManager {
|
||||
view_id,
|
||||
meta: Default::default(),
|
||||
set_as_current: false,
|
||||
index: None,
|
||||
};
|
||||
|
||||
let view = create_view(params, import_data.view_layout);
|
||||
self.with_folder((), |folder| {
|
||||
folder.insert_view(view.clone());
|
||||
folder.insert_view(view.clone(), None);
|
||||
});
|
||||
notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]);
|
||||
Ok(view)
|
||||
|
@ -34,6 +34,9 @@ pub enum FolderNotification {
|
||||
DidUpdateTrash = 15,
|
||||
DidUpdateFolderSnapshotState = 16,
|
||||
DidUpdateFolderSyncUpdate = 17,
|
||||
|
||||
DidFavoriteView = 36,
|
||||
DidUnfavoriteView = 37,
|
||||
}
|
||||
|
||||
impl std::convert::From<FolderNotification> for i32 {
|
||||
@ -57,6 +60,8 @@ impl std::convert::From<i32> for FolderNotification {
|
||||
15 => FolderNotification::DidUpdateTrash,
|
||||
16 => FolderNotification::DidUpdateFolderSnapshotState,
|
||||
17 => FolderNotification::DidUpdateFolderSyncUpdate,
|
||||
36 => FolderNotification::DidFavoriteView,
|
||||
37 => FolderNotification::DidUnfavoriteView,
|
||||
_ => FolderNotification::Unknown,
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ impl FolderManager {
|
||||
initial_data: vec![],
|
||||
meta: ext,
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
self.create_view_with_params(params).await.unwrap();
|
||||
view_id
|
||||
|
@ -54,6 +54,7 @@ pub struct ViewBuilder {
|
||||
desc: String,
|
||||
layout: ViewLayout,
|
||||
child_views: Vec<ParentChildViews>,
|
||||
is_favorite: bool,
|
||||
icon_url: Option<String>,
|
||||
cover_url: Option<String>,
|
||||
}
|
||||
@ -67,6 +68,7 @@ impl ViewBuilder {
|
||||
desc: Default::default(),
|
||||
layout: ViewLayout::Document,
|
||||
child_views: vec![],
|
||||
is_favorite: false,
|
||||
icon_url: None,
|
||||
cover_url: None,
|
||||
}
|
||||
@ -110,6 +112,7 @@ impl ViewBuilder {
|
||||
name: self.name,
|
||||
desc: self.desc,
|
||||
created_at: timestamp(),
|
||||
is_favorite: self.is_favorite,
|
||||
layout: self.layout,
|
||||
icon_url: self.icon_url,
|
||||
cover_url: self.cover_url,
|
||||
@ -252,9 +255,10 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View
|
||||
desc: params.desc,
|
||||
children: Default::default(),
|
||||
created_at: time,
|
||||
is_favorite: false,
|
||||
layout,
|
||||
icon_url: None,
|
||||
cover_url: None,
|
||||
icon_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +98,7 @@ async fn update_parent_view_test() {
|
||||
UpdateParentView {
|
||||
name: Some(new_name.clone()),
|
||||
desc: None,
|
||||
is_favorite: None,
|
||||
},
|
||||
ReloadParentView(parent_view.id),
|
||||
])
|
||||
@ -143,6 +144,7 @@ async fn view_update() {
|
||||
UpdateView {
|
||||
name: Some(new_name.clone()),
|
||||
desc: None,
|
||||
is_favorite: None,
|
||||
},
|
||||
ReadView(view.id),
|
||||
])
|
||||
@ -249,6 +251,56 @@ async fn view_delete_all_permanent() {
|
||||
assert_eq!(test.trash.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toggle_favorites() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.child_view.clone();
|
||||
test
|
||||
.run_scripts(vec![
|
||||
ReadView(view.id.clone()),
|
||||
ToggleFavorite,
|
||||
ReadFavorites,
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.child_view.is_favorite, true);
|
||||
assert!(test.favorites.len() != 0);
|
||||
assert_eq!(test.favorites[0].id, view.id);
|
||||
|
||||
let view = test.child_view.clone();
|
||||
test
|
||||
.run_scripts(vec![
|
||||
ReadView(view.id.clone()),
|
||||
ToggleFavorite,
|
||||
ReadFavorites,
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
|
||||
assert!(!test.child_view.is_favorite);
|
||||
assert!(test.favorites.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_favorites() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.child_view.clone();
|
||||
test
|
||||
.run_scripts(vec![
|
||||
ReadView(view.id.clone()),
|
||||
ToggleFavorite,
|
||||
ReadFavorites,
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.child_view.is_favorite, true);
|
||||
assert!(test.favorites.len() != 0);
|
||||
assert_eq!(test.favorites[0].id, view.id);
|
||||
|
||||
test.run_scripts(vec![DeleteView, ReadFavorites]).await;
|
||||
assert!(test.favorites.len() == 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_view_event_test() {
|
||||
let mut test = FolderTest::new().await;
|
||||
|
@ -25,6 +25,7 @@ pub enum FolderScript {
|
||||
UpdateParentView {
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
},
|
||||
DeleteParentView,
|
||||
|
||||
@ -39,6 +40,7 @@ pub enum FolderScript {
|
||||
UpdateView {
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
},
|
||||
DeleteView,
|
||||
DeleteViews(Vec<String>),
|
||||
@ -53,6 +55,8 @@ pub enum FolderScript {
|
||||
RestoreViewFromTrash,
|
||||
ReadTrash,
|
||||
DeleteAllTrash,
|
||||
ToggleFavorite,
|
||||
ReadFavorites,
|
||||
}
|
||||
|
||||
pub struct FolderTest {
|
||||
@ -62,6 +66,7 @@ pub struct FolderTest {
|
||||
pub parent_view: ViewPB,
|
||||
pub child_view: ViewPB,
|
||||
pub trash: Vec<TrashPB>,
|
||||
pub favorites: Vec<ViewPB>,
|
||||
}
|
||||
|
||||
impl FolderTest {
|
||||
@ -85,6 +90,7 @@ impl FolderTest {
|
||||
parent_view,
|
||||
child_view: view,
|
||||
trash: vec![],
|
||||
favorites: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +129,12 @@ impl FolderTest {
|
||||
let parent_view = read_view(sdk, &parent_view_id).await;
|
||||
self.parent_view = parent_view;
|
||||
},
|
||||
FolderScript::UpdateParentView { name, desc } => {
|
||||
update_view(sdk, &self.parent_view.id, name, desc).await;
|
||||
FolderScript::UpdateParentView {
|
||||
name,
|
||||
desc,
|
||||
is_favorite,
|
||||
} => {
|
||||
update_view(sdk, &self.parent_view.id, name, desc, is_favorite).await;
|
||||
},
|
||||
FolderScript::DeleteParentView => {
|
||||
delete_view(sdk, vec![self.parent_view.id.clone()]).await;
|
||||
@ -147,8 +157,12 @@ impl FolderTest {
|
||||
let view = read_view(sdk, &view_id).await;
|
||||
self.child_view = view;
|
||||
},
|
||||
FolderScript::UpdateView { name, desc } => {
|
||||
update_view(sdk, &self.child_view.id, name, desc).await;
|
||||
FolderScript::UpdateView {
|
||||
name,
|
||||
desc,
|
||||
is_favorite,
|
||||
} => {
|
||||
update_view(sdk, &self.child_view.id, name, desc, is_favorite).await;
|
||||
},
|
||||
FolderScript::DeleteView => {
|
||||
delete_view(sdk, vec![self.child_view.id.clone()]).await;
|
||||
@ -170,6 +184,13 @@ impl FolderTest {
|
||||
delete_all_trash(sdk).await;
|
||||
self.trash = vec![];
|
||||
},
|
||||
FolderScript::ToggleFavorite => {
|
||||
toggle_favorites(sdk, vec![self.child_view.id.clone()]).await;
|
||||
},
|
||||
FolderScript::ReadFavorites => {
|
||||
let favorites = read_favorites(sdk).await;
|
||||
self.favorites = favorites.to_vec();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -223,6 +244,7 @@ pub async fn create_app(sdk: &FlowyCoreTest, workspace_id: &str, name: &str, des
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
|
||||
EventBuilder::new(sdk.clone())
|
||||
@ -249,6 +271,7 @@ pub async fn create_view(
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(sdk.clone())
|
||||
.event(CreateView)
|
||||
@ -293,11 +316,14 @@ pub async fn update_view(
|
||||
view_id: &str,
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
) {
|
||||
println!("Toggling update view {:?}", is_favorite);
|
||||
let request = UpdateViewPayloadPB {
|
||||
view_id: view_id.to_string(),
|
||||
name,
|
||||
desc,
|
||||
is_favorite,
|
||||
..Default::default()
|
||||
};
|
||||
EventBuilder::new(sdk.clone())
|
||||
@ -352,3 +378,20 @@ pub async fn delete_all_trash(sdk: &FlowyCoreTest) {
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn toggle_favorites(sdk: &FlowyCoreTest, view_id: Vec<String>) {
|
||||
let request = RepeatedViewIdPB { items: view_id };
|
||||
EventBuilder::new(sdk.clone())
|
||||
.event(ToggleFavorite)
|
||||
.payload(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn read_favorites(sdk: &FlowyCoreTest) -> RepeatedViewPB {
|
||||
EventBuilder::new(sdk.clone())
|
||||
.event(ReadFavorites)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<RepeatedViewPB>()
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ impl DocumentEventTest {
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(core.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
|
@ -77,6 +77,7 @@ async fn create_app(sdk: &FlowyCoreTest, name: &str, desc: &str, workspace_id: &
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
|
||||
EventBuilder::new(sdk.clone())
|
||||
@ -102,6 +103,7 @@ async fn create_view(
|
||||
initial_data: data,
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
|
||||
EventBuilder::new(sdk.clone())
|
||||
|
@ -190,6 +190,7 @@ impl FlowyCoreTest {
|
||||
initial_data: vec![],
|
||||
meta: Default::default(),
|
||||
set_as_current: false,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
@ -214,6 +215,7 @@ impl FlowyCoreTest {
|
||||
initial_data,
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
let view = EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
@ -246,6 +248,7 @@ impl FlowyCoreTest {
|
||||
initial_data,
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
@ -275,6 +278,7 @@ impl FlowyCoreTest {
|
||||
initial_data,
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
@ -299,6 +303,7 @@ impl FlowyCoreTest {
|
||||
initial_data,
|
||||
meta: Default::default(),
|
||||
set_as_current: true,
|
||||
index: None,
|
||||
};
|
||||
EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::CreateView)
|
||||
|
Loading…
Reference in New Issue
Block a user