mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: sidebar UI Revamp on Desktop (#5343)
This commit is contained in:
parent
13b3439bd6
commit
a8f136eda2
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||||
@ -12,14 +10,15 @@ import 'package:appflowy/startup/startup.dart';
|
|||||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'board_row_test.dart' as board_row_test;
|
|
||||||
import 'board_add_row_test.dart' as board_add_row_test;
|
import 'board_add_row_test.dart' as board_add_row_test;
|
||||||
import 'board_group_test.dart' as board_group_test;
|
import 'board_group_test.dart' as board_group_test;
|
||||||
|
import 'board_row_test.dart' as board_row_test;
|
||||||
|
|
||||||
void startTesting() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Board integration tests
|
// Board integration tests
|
||||||
|
@ -19,12 +19,14 @@ void main() {
|
|||||||
// Duplicate
|
// Duplicate
|
||||||
await tester.openMoreViewActions();
|
await tester.openMoreViewActions();
|
||||||
await tester.duplicateByMoreViewActions();
|
await tester.duplicateByMoreViewActions();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(pageFinder, findsNWidgets(2));
|
expect(pageFinder, findsNWidgets(2));
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
await tester.openMoreViewActions();
|
await tester.openMoreViewActions();
|
||||||
await tester.deleteByMoreViewActions();
|
await tester.deleteByMoreViewActions();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(pageFinder, findsNWidgets(1));
|
expect(pageFinder, findsNWidgets(1));
|
||||||
});
|
});
|
||||||
|
@ -130,24 +130,24 @@ void main() {
|
|||||||
final searchEmojiTextField = find.byWidgetPredicate(
|
final searchEmojiTextField = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is TextField &&
|
widget is TextField &&
|
||||||
widget.decoration!.hintText == LocaleKeys.emoji_search.tr(),
|
widget.decoration!.hintText == LocaleKeys.search_label.tr(),
|
||||||
);
|
);
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
searchEmojiTextField,
|
searchEmojiTextField,
|
||||||
'hand',
|
'punch',
|
||||||
);
|
);
|
||||||
|
|
||||||
// change skin tone
|
// change skin tone
|
||||||
await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark);
|
await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark);
|
||||||
|
|
||||||
// select an icon with skin tone
|
// select an icon with skin tone
|
||||||
const hand = '👋🏿';
|
const punch = '👊🏿';
|
||||||
await tester.tapEmoji(hand);
|
await tester.tapEmoji(punch);
|
||||||
tester.expectToSeeDocumentIcon(hand);
|
tester.expectToSeeDocumentIcon(punch);
|
||||||
tester.expectViewHasIcon(
|
tester.expectViewHasIcon(
|
||||||
gettingStarted,
|
gettingStarted,
|
||||||
ViewLayoutPB.Document,
|
ViewLayoutPB.Document,
|
||||||
hand,
|
punch,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -12,8 +12,8 @@ void main() {
|
|||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('sidebar expand test', () {
|
group('sidebar expand test', () {
|
||||||
bool isExpanded({required FolderCategoryType type}) {
|
bool isExpanded({required FolderSpaceType type}) {
|
||||||
if (type == FolderCategoryType.private) {
|
if (type == FolderSpaceType.private) {
|
||||||
return find
|
return find
|
||||||
.descendant(
|
.descendant(
|
||||||
of: find.byType(PrivateSectionFolder),
|
of: find.byType(PrivateSectionFolder),
|
||||||
@ -30,19 +30,19 @@ void main() {
|
|||||||
await tester.tapAnonymousSignInButton();
|
await tester.tapAnonymousSignInButton();
|
||||||
|
|
||||||
// first time is expanded
|
// first time is expanded
|
||||||
expect(isExpanded(type: FolderCategoryType.private), true);
|
expect(isExpanded(type: FolderSpaceType.private), true);
|
||||||
|
|
||||||
// collapse the personal folder
|
// collapse the personal folder
|
||||||
await tester.tapButton(
|
await tester.tapButton(
|
||||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
||||||
);
|
);
|
||||||
expect(isExpanded(type: FolderCategoryType.private), false);
|
expect(isExpanded(type: FolderSpaceType.private), false);
|
||||||
|
|
||||||
// expand the personal folder
|
// expand the personal folder
|
||||||
await tester.tapButton(
|
await tester.tapButton(
|
||||||
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
|
||||||
);
|
);
|
||||||
expect(isExpanded(type: FolderCategoryType.private), true);
|
expect(isExpanded(type: FolderSpaceType.private), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
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/sidebar/favorites/favorite_folder.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
@ -46,7 +46,7 @@ void main() {
|
|||||||
await tester.favoriteViewByName(names[1]);
|
await tester.favoriteViewByName(names[1]);
|
||||||
expect(
|
expect(
|
||||||
tester.findFavoritePageName(names[1]),
|
tester.findFavoritePageName(names[1]),
|
||||||
findsNWidgets(2),
|
findsNWidgets(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.unfavoriteViewByName(gettingStarted);
|
await tester.unfavoriteViewByName(gettingStarted);
|
||||||
@ -120,9 +120,9 @@ void main() {
|
|||||||
(widget) =>
|
(widget) =>
|
||||||
widget is SingleInnerViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
widget.spaceType == FolderSpaceType.favorite,
|
||||||
),
|
),
|
||||||
findsNWidgets(6),
|
findsNWidgets(3),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.hoverOnPageName(
|
await tester.hoverOnPageName(
|
||||||
@ -135,7 +135,7 @@ void main() {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
tester.findAllFavoritePages(),
|
tester.findAllFavoritePages(),
|
||||||
findsNWidgets(3),
|
findsNWidgets(2),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.hoverOnPageName(
|
await tester.hoverOnPageName(
|
||||||
@ -168,7 +168,7 @@ void main() {
|
|||||||
widget.isSelected != null &&
|
widget.isSelected != null &&
|
||||||
widget.isSelected!(),
|
widget.isSelected!(),
|
||||||
),
|
),
|
||||||
findsNWidgets(2),
|
findsNWidgets(1),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
|||||||
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||||
import 'sidebar_test.dart' as sidebar_test;
|
import 'sidebar_test.dart' as sidebar_test;
|
||||||
|
|
||||||
void startTesting() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Sidebar integration tests
|
// Sidebar integration tests
|
||||||
|
@ -27,7 +27,7 @@ Future<void> runIntegration3OnDesktop() async {
|
|||||||
settings_test_runner.main();
|
settings_test_runner.main();
|
||||||
share_markdown_test.main();
|
share_markdown_test.main();
|
||||||
import_files_test.main();
|
import_files_test.main();
|
||||||
sidebar_test_runner.startTesting();
|
sidebar_test_runner.main();
|
||||||
board_test_runner.startTesting();
|
board_test_runner.main();
|
||||||
tabs_test.main();
|
tabs_test.main();
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv.dart';
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
@ -16,9 +11,9 @@ import 'package:appflowy/shared/feature_flags.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.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';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||||
@ -34,6 +29,10 @@ import 'package:appflowy_backend/log.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'emoji.dart';
|
import 'emoji.dart';
|
||||||
@ -60,6 +59,7 @@ extension CommonOperations on WidgetTester {
|
|||||||
/// Tap the + button on the home page.
|
/// Tap the + button on the home page.
|
||||||
Future<void> tapAddViewButton({
|
Future<void> tapAddViewButton({
|
||||||
String name = gettingStarted,
|
String name = gettingStarted,
|
||||||
|
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||||
}) async {
|
}) async {
|
||||||
await hoverOnPageName(
|
await hoverOnPageName(
|
||||||
name,
|
name,
|
||||||
@ -279,7 +279,7 @@ extension CommonOperations on WidgetTester {
|
|||||||
bool openAfterCreated = true,
|
bool openAfterCreated = true,
|
||||||
}) async {
|
}) async {
|
||||||
// create a new page
|
// create a new page
|
||||||
await tapAddViewButton(name: parentName ?? gettingStarted);
|
await tapAddViewButton(name: parentName ?? gettingStarted, layout: layout);
|
||||||
await tapButtonWithName(layout.menuName);
|
await tapButtonWithName(layout.menuName);
|
||||||
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
|
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
|
||||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||||
|
@ -81,15 +81,12 @@ class EditorOperations {
|
|||||||
|
|
||||||
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
|
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
|
||||||
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
|
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
|
||||||
Finder button =
|
final Finder button = !isInPicker
|
||||||
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
|
? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())
|
||||||
if (isInPicker) {
|
: find.descendant(
|
||||||
button = find.descendant(
|
|
||||||
of: find.byType(FlowyIconPicker),
|
of: find.byType(FlowyIconPicker),
|
||||||
matching: button,
|
matching: find.text(LocaleKeys.button_remove.tr()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
await tester.tapButton(button);
|
await tester.tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ extension Expectation on WidgetTester {
|
|||||||
(widget) =>
|
(widget) =>
|
||||||
widget is SingleInnerViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite &&
|
widget.spaceType == FolderSpaceType.favorite &&
|
||||||
widget.view.name == name &&
|
widget.view.name == name &&
|
||||||
widget.view.layout == layout,
|
widget.view.layout == layout,
|
||||||
skipOffstage: false,
|
skipOffstage: false,
|
||||||
@ -175,7 +175,7 @@ extension Expectation on WidgetTester {
|
|||||||
(widget) =>
|
(widget) =>
|
||||||
widget is SingleInnerViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
widget.spaceType == FolderSpaceType.favorite,
|
||||||
);
|
);
|
||||||
|
|
||||||
Finder findPageName(
|
Finder findPageName(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||||
@ -12,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../desktop/board/board_hide_groups_test.dart';
|
import '../desktop/board/board_hide_groups_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
import 'common_operations.dart';
|
import 'common_operations.dart';
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'util.dart';
|
||||||
|
|
||||||
extension AppFlowyWorkspace on WidgetTester {
|
extension AppFlowyWorkspace on WidgetTester {
|
||||||
/// Open workspace menu
|
/// Open workspace menu
|
||||||
@ -36,12 +36,19 @@ extension AppFlowyWorkspace on WidgetTester {
|
|||||||
matching: find.byType(WorkspaceMoreActionList),
|
matching: find.byType(WorkspaceMoreActionList),
|
||||||
);
|
);
|
||||||
expect(moreButton, findsOneWidget);
|
expect(moreButton, findsOneWidget);
|
||||||
|
await hoverOnWidget(
|
||||||
|
moreButton,
|
||||||
|
onHover: () async {
|
||||||
await tapButton(moreButton);
|
await tapButton(moreButton);
|
||||||
await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr()));
|
await tapButton(
|
||||||
|
find.findTextInFlowyText(LocaleKeys.button_rename.tr()),
|
||||||
|
);
|
||||||
final input = find.byType(TextFormField);
|
final input = find.byType(TextFormField);
|
||||||
expect(input, findsOneWidget);
|
expect(input, findsOneWidget);
|
||||||
await enterText(input, name);
|
await enterText(input, name);
|
||||||
await tapButton(find.text(LocaleKeys.button_ok.tr()));
|
await tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> changeWorkspaceIcon(String icon) async {
|
Future<void> changeWorkspaceIcon(String icon) async {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TypeOptionMenuItemValue<T> {
|
class TypeOptionMenuItemValue<T> {
|
||||||
|
@ -38,6 +38,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||||||
case MobileViewBottomSheetBodyAction.removeFromFavorites:
|
case MobileViewBottomSheetBodyAction.removeFromFavorites:
|
||||||
context.pop();
|
context.pop();
|
||||||
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
|
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case MobileViewBottomSheetBodyAction.undo:
|
case MobileViewBottomSheetBodyAction.undo:
|
||||||
EditorNotification.undo().post();
|
EditorNotification.undo().post();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension BottomSheetPaddingExtension on BuildContext {
|
extension BottomSheetPaddingExtension on BuildContext {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart';
|
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart';
|
||||||
@ -7,6 +5,7 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
|||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class MobileFavoriteFolder extends StatelessWidget {
|
class MobileFavoriteFolder extends StatelessWidget {
|
||||||
@ -28,7 +27,7 @@ class MobileFavoriteFolder extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return BlocProvider<FolderBloc>(
|
return BlocProvider<FolderBloc>(
|
||||||
create: (context) => FolderBloc(type: FolderCategoryType.favorite)
|
create: (context) => FolderBloc(type: FolderSpaceType.favorite)
|
||||||
..add(
|
..add(
|
||||||
const FolderEvent.initial(),
|
const FolderEvent.initial(),
|
||||||
),
|
),
|
||||||
@ -55,9 +54,9 @@ class MobileFavoriteFolder extends StatelessWidget {
|
|||||||
...views.map(
|
...views.map(
|
||||||
(view) => MobileViewItem(
|
(view) => MobileViewItem(
|
||||||
key: ValueKey(
|
key: ValueKey(
|
||||||
'${FolderCategoryType.favorite.name} ${view.id}',
|
'${FolderSpaceType.favorite.name} ${view.id}',
|
||||||
),
|
),
|
||||||
categoryType: FolderCategoryType.favorite,
|
spaceType: FolderSpaceType.favorite,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
isFirstChild: view.id == views.first.id,
|
isFirstChild: view.id == views.first.id,
|
||||||
isFeedback: false,
|
isFeedback: false,
|
||||||
|
@ -70,20 +70,20 @@ class MobileFolders extends StatelessWidget {
|
|||||||
? [
|
? [
|
||||||
MobileSectionFolder(
|
MobileSectionFolder(
|
||||||
title: LocaleKeys.sideBar_workspace.tr(),
|
title: LocaleKeys.sideBar_workspace.tr(),
|
||||||
categoryType: FolderCategoryType.public,
|
spaceType: FolderSpaceType.public,
|
||||||
views: state.section.publicViews,
|
views: state.section.publicViews,
|
||||||
),
|
),
|
||||||
const VSpace(8.0),
|
const VSpace(8.0),
|
||||||
MobileSectionFolder(
|
MobileSectionFolder(
|
||||||
title: LocaleKeys.sideBar_private.tr(),
|
title: LocaleKeys.sideBar_private.tr(),
|
||||||
categoryType: FolderCategoryType.private,
|
spaceType: FolderSpaceType.private,
|
||||||
views: state.section.privateViews,
|
views: state.section.privateViews,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
MobileSectionFolder(
|
MobileSectionFolder(
|
||||||
title: LocaleKeys.sideBar_personal.tr(),
|
title: LocaleKeys.sideBar_personal.tr(),
|
||||||
categoryType: FolderCategoryType.public,
|
spaceType: FolderSpaceType.public,
|
||||||
views: state.section.publicViews,
|
views: state.section.publicViews,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
@ -15,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@ -126,6 +125,7 @@ class _MobileWorkspace extends StatelessWidget {
|
|||||||
child: WorkspaceIcon(
|
child: WorkspaceIcon(
|
||||||
workspace: currentWorkspace,
|
workspace: currentWorkspace,
|
||||||
iconSize: 26,
|
iconSize: 26,
|
||||||
|
fontSize: 16.0,
|
||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
|
||||||
@ -8,9 +6,11 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
|||||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class MobileSectionFolder extends StatelessWidget {
|
class MobileSectionFolder extends StatelessWidget {
|
||||||
@ -18,17 +18,17 @@ class MobileSectionFolder extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.views,
|
required this.views,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final List<ViewPB> views;
|
final List<ViewPB> views;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<FolderBloc>(
|
return BlocProvider<FolderBloc>(
|
||||||
create: (context) => FolderBloc(type: categoryType)
|
create: (context) => FolderBloc(type: spaceType)
|
||||||
..add(
|
..add(
|
||||||
const FolderEvent.initial(),
|
const FolderEvent.initial(),
|
||||||
),
|
),
|
||||||
@ -48,7 +48,7 @@ class MobileSectionFolder extends StatelessWidget {
|
|||||||
name:
|
name:
|
||||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
index: 0,
|
index: 0,
|
||||||
viewSection: categoryType.toViewSectionPB,
|
viewSection: spaceType.toViewSectionPB,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
context.read<FolderBloc>().add(
|
context.read<FolderBloc>().add(
|
||||||
@ -64,13 +64,13 @@ class MobileSectionFolder extends StatelessWidget {
|
|||||||
...views.map(
|
...views.map(
|
||||||
(view) => MobileViewItem(
|
(view) => MobileViewItem(
|
||||||
key: ValueKey(
|
key: ValueKey(
|
||||||
'${FolderCategoryType.private.name} ${view.id}',
|
'${FolderSpaceType.private.name} ${view.id}',
|
||||||
),
|
),
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
isFirstChild: view.id == views.first.id,
|
isFirstChild: view.id == views.first.id,
|
||||||
view: view,
|
view: view,
|
||||||
level: 0,
|
level: 0,
|
||||||
leftPadding: 16,
|
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||||
isFeedback: false,
|
isFeedback: false,
|
||||||
onSelected: context.pushView,
|
onSelected: context.pushView,
|
||||||
endActionPane: (context) {
|
endActionPane: (context) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
@ -9,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
// Only works on mobile.
|
// Only works on mobile.
|
||||||
@ -106,6 +105,7 @@ class _WorkspaceMenuItem extends StatelessWidget {
|
|||||||
leftIcon: WorkspaceIcon(
|
leftIcon: WorkspaceIcon(
|
||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
iconSize: 26,
|
iconSize: 26,
|
||||||
|
fontSize: 16.0,
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
@ -12,6 +10,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_it
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ class MobileViewItem extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
this.parentView,
|
this.parentView,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
required this.level,
|
required this.level,
|
||||||
this.leftPadding = 10,
|
this.leftPadding = 10,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
@ -39,7 +38,7 @@ class MobileViewItem extends StatelessWidget {
|
|||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final ViewPB? parentView;
|
final ViewPB? parentView;
|
||||||
|
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
// indicate the level of the view item
|
// indicate the level of the view item
|
||||||
// used to calculate the left padding
|
// used to calculate the left padding
|
||||||
@ -80,7 +79,7 @@ class MobileViewItem extends StatelessWidget {
|
|||||||
view: state.view,
|
view: state.view,
|
||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
childViews: state.view.childViews,
|
childViews: state.view.childViews,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
level: level,
|
level: level,
|
||||||
leftPadding: leftPadding,
|
leftPadding: leftPadding,
|
||||||
showActions: true,
|
showActions: true,
|
||||||
@ -104,7 +103,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
|||||||
required this.view,
|
required this.view,
|
||||||
required this.parentView,
|
required this.parentView,
|
||||||
required this.childViews,
|
required this.childViews,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
this.isExpanded = true,
|
this.isExpanded = true,
|
||||||
required this.level,
|
required this.level,
|
||||||
@ -120,7 +119,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
|||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final ViewPB? parentView;
|
final ViewPB? parentView;
|
||||||
final List<ViewPB> childViews;
|
final List<ViewPB> childViews;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
final bool isDraggable;
|
final bool isDraggable;
|
||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
@ -144,7 +143,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
|||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
level: level,
|
level: level,
|
||||||
showActions: showActions,
|
showActions: showActions,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
isExpanded: isExpanded,
|
isExpanded: isExpanded,
|
||||||
isDraggable: isDraggable,
|
isDraggable: isDraggable,
|
||||||
@ -159,9 +158,9 @@ class InnerMobileViewItem extends StatelessWidget {
|
|||||||
if (childViews.isNotEmpty) {
|
if (childViews.isNotEmpty) {
|
||||||
final children = childViews.map((childView) {
|
final children = childViews.map((childView) {
|
||||||
return MobileViewItem(
|
return MobileViewItem(
|
||||||
key: ValueKey('${categoryType.name} ${childView.id}'),
|
key: ValueKey('${spaceType.name} ${childView.id}'),
|
||||||
parentView: view,
|
parentView: view,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
isFirstChild: childView.id == childViews.first.id,
|
isFirstChild: childView.id == childViews.first.id,
|
||||||
view: childView,
|
view: childView,
|
||||||
level: level + 1,
|
level: level + 1,
|
||||||
@ -235,7 +234,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
|||||||
return MobileViewItem(
|
return MobileViewItem(
|
||||||
view: view,
|
view: view,
|
||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
level: level,
|
level: level,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
@ -262,7 +261,7 @@ class SingleMobileInnerViewItem extends StatefulWidget {
|
|||||||
required this.level,
|
required this.level,
|
||||||
required this.leftPadding,
|
required this.leftPadding,
|
||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
required this.showActions,
|
required this.showActions,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
required this.isFeedback,
|
required this.isFeedback,
|
||||||
@ -282,7 +281,7 @@ class SingleMobileInnerViewItem extends StatefulWidget {
|
|||||||
final bool isDraggable;
|
final bool isDraggable;
|
||||||
final bool showActions;
|
final bool showActions;
|
||||||
final ViewItemOnSelected onSelected;
|
final ViewItemOnSelected onSelected;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
final ActionPaneBuilder? startActionPane;
|
final ActionPaneBuilder? startActionPane;
|
||||||
final ActionPaneBuilder? endActionPane;
|
final ActionPaneBuilder? endActionPane;
|
||||||
|
|
||||||
@ -407,9 +406,8 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
|||||||
ViewEvent.createView(
|
ViewEvent.createView(
|
||||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
layout,
|
layout,
|
||||||
section:
|
section: widget.spaceType != FolderSpaceType.favorite
|
||||||
widget.categoryType != FolderCategoryType.favorite
|
? widget.spaceType.toViewSectionPB
|
||||||
? widget.categoryType.toViewSectionPB
|
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
// use a global value to store the selected emoji to prevent reloading every time.
|
// use a global value to store the selected emoji to prevent reloading every time.
|
||||||
EmojiData? kCachedEmojiData;
|
EmojiData? kCachedEmojiData;
|
||||||
@ -28,7 +25,6 @@ class FlowyEmojiPicker extends StatefulWidget {
|
|||||||
|
|
||||||
class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||||
EmojiData? emojiData;
|
EmojiData? emojiData;
|
||||||
List<String>? fallbackFontFamily;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -47,13 +43,6 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.isAndroid || Platform.isLinux) {
|
|
||||||
final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily;
|
|
||||||
if (notoColorEmoji != null) {
|
|
||||||
fallbackFontFamily = [notoColorEmoji];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -83,16 +72,18 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, emojiId, emoji, callback) {
|
itemBuilder: (context, emojiId, emoji, callback) {
|
||||||
return FlowyIconButton(
|
return SizedBox(
|
||||||
iconPadding: PlatformExtension.isWindows
|
width: 36,
|
||||||
? const EdgeInsets.only(bottom: 2.0)
|
height: 36,
|
||||||
: const EdgeInsets.all(2),
|
child: FlowyButton(
|
||||||
icon: FlowyText(
|
margin: EdgeInsets.zero,
|
||||||
|
radius: Corners.s8Border,
|
||||||
|
text: FlowyText.emoji(
|
||||||
emoji,
|
emoji,
|
||||||
fontSize: 28.0,
|
fontSize: 24.0,
|
||||||
fallbackFontFamily: fallbackFontFamily,
|
),
|
||||||
|
onTap: () => callback(emojiId, emoji),
|
||||||
),
|
),
|
||||||
onPressed: () => callback(emojiId, emoji),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
searchBarBuilder: (context, keyword, skinTone) {
|
searchBarBuilder: (context, keyword, skinTone) {
|
||||||
|
@ -16,9 +16,14 @@ class FlowyEmojiHeader extends StatelessWidget {
|
|||||||
if (PlatformExtension.isDesktopOrWeb) {
|
if (PlatformExtension.isDesktopOrWeb) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 22,
|
height: 22,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
child: FlowyText.regular(category.id),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
category.id,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -42,7 +42,7 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 12.0,
|
||||||
horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0,
|
horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -52,16 +52,15 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
|||||||
onKeywordChanged: widget.onKeywordChanged,
|
onKeywordChanged: widget.onKeywordChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(6.0),
|
const HSpace(8.0),
|
||||||
_RandomEmojiButton(
|
_RandomEmojiButton(
|
||||||
emojiData: widget.emojiData,
|
emojiData: widget.emojiData,
|
||||||
onRandomEmojiSelected: widget.onRandomEmojiSelected,
|
onRandomEmojiSelected: widget.onRandomEmojiSelected,
|
||||||
),
|
),
|
||||||
const HSpace(6.0),
|
const HSpace(8.0),
|
||||||
FlowyEmojiSkinToneSelector(
|
FlowyEmojiSkinToneSelector(
|
||||||
onEmojiSkinToneChanged: widget.onSkinToneChanged,
|
onEmojiSkinToneChanged: widget.onSkinToneChanged,
|
||||||
),
|
),
|
||||||
const HSpace(6.0),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -79,12 +78,21 @@ class _RandomEmojiButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyTooltip(
|
return Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: const BorderSide(color: Color(0x1E171717)),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.emoji_random.tr(),
|
message: LocaleKeys.emoji_random.tr(),
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: const Icon(
|
text: const FlowySvg(
|
||||||
Icons.shuffle_rounded,
|
FlowySvgs.icon_shuffle_s,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final random = emojiData.random;
|
final random = emojiData.random;
|
||||||
@ -94,6 +102,7 @@ class _RandomEmojiButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,32 +132,35 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ConstrainedBox(
|
return SizedBox(
|
||||||
constraints: const BoxConstraints(
|
height: 36.0,
|
||||||
maxHeight: 32.0,
|
|
||||||
),
|
|
||||||
child: FlowyTextField(
|
child: FlowyTextField(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
hintText: LocaleKeys.emoji_search.tr(),
|
hintText: LocaleKeys.search_label.tr(),
|
||||||
|
hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onChanged: widget.onKeywordChanged,
|
onChanged: widget.onKeywordChanged,
|
||||||
prefixIcon: const Padding(
|
prefixIcon: const Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 8.0,
|
left: 14.0,
|
||||||
right: 4.0,
|
right: 8.0,
|
||||||
),
|
),
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
FlowySvgs.search_s,
|
FlowySvgs.search_s,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
prefixIconConstraints: const BoxConstraints(
|
prefixIconConstraints: const BoxConstraints(
|
||||||
maxHeight: 18.0,
|
maxHeight: 20.0,
|
||||||
),
|
),
|
||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: const EdgeInsets.all(4.0),
|
padding: const EdgeInsets.all(4.0),
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: const FlowySvg(
|
text: const FlowySvg(
|
||||||
FlowySvgs.close_lg,
|
FlowySvgs.m_app_bar_close_s,
|
||||||
),
|
),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
|
@ -57,7 +57,7 @@ class _FlowyEmojiSkinToneSelectorState
|
|||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.emoji_selectSkinTone.tr(),
|
message: LocaleKeys.emoji_selectSkinTone.tr(),
|
||||||
child: _buildIconButton(
|
child: _buildIconButton(
|
||||||
lastSelectedEmojiSkinTone?.icon ?? '✋',
|
lastSelectedEmojiSkinTone?.icon ?? '👋',
|
||||||
() => controller.show(),
|
() => controller.show(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -65,19 +65,22 @@ class _FlowyEmojiSkinToneSelectorState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIconButton(String icon, VoidCallback onPressed) {
|
Widget _buildIconButton(String icon, VoidCallback onPressed) {
|
||||||
return FlowyIconButton(
|
return Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: const Color(0x1E171717)),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: FlowyButton(
|
||||||
key: emojiSkinToneKey(icon),
|
key: emojiSkinToneKey(icon),
|
||||||
icon: Padding(
|
margin: EdgeInsets.zero,
|
||||||
// add a left padding to align the emoji center
|
text: FlowyText.emoji(
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 3.0,
|
|
||||||
),
|
|
||||||
child: FlowyText(
|
|
||||||
icon,
|
icon,
|
||||||
fontSize: 22.0,
|
fontSize: 24.0,
|
||||||
),
|
),
|
||||||
|
onTap: onPressed,
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,17 +89,17 @@ extension EmojiSkinToneIcon on EmojiSkinTone {
|
|||||||
String get icon {
|
String get icon {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case EmojiSkinTone.none:
|
case EmojiSkinTone.none:
|
||||||
return '✋';
|
return '👋';
|
||||||
case EmojiSkinTone.light:
|
case EmojiSkinTone.light:
|
||||||
return '✋🏻';
|
return '👋🏻';
|
||||||
case EmojiSkinTone.mediumLight:
|
case EmojiSkinTone.mediumLight:
|
||||||
return '✋🏼';
|
return '👋🏼';
|
||||||
case EmojiSkinTone.medium:
|
case EmojiSkinTone.medium:
|
||||||
return '✋🏽';
|
return '👋🏽';
|
||||||
case EmojiSkinTone.mediumDark:
|
case EmojiSkinTone.mediumDark:
|
||||||
return '✋🏾';
|
return '👋🏾';
|
||||||
case EmojiSkinTone.dark:
|
case EmojiSkinTone.dark:
|
||||||
return '✋🏿';
|
return '👋🏿';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ class EmojiText extends StatelessWidget {
|
|||||||
emoji,
|
emoji,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
|
strutStyle: const StrutStyle(forceStrutHeight: true),
|
||||||
fallbackFontFamily: _cachedFallbackFontFamily,
|
fallbackFontFamily: _cachedFallbackFontFamily,
|
||||||
lineHeight: lineHeight,
|
lineHeight: lineHeight,
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension ToProto on FlowyIconType {
|
extension ToProto on FlowyIconType {
|
||||||
ViewIconTypePB toProto() {
|
ViewIconTypePB toProto() {
|
||||||
@ -54,58 +51,29 @@ class FlowyIconPicker extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// ONLY supports emoji picker for now
|
return Padding(
|
||||||
return DefaultTabController(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
length: 1,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
const VSpace(8.0),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTabs(context),
|
FlowyText(LocaleKeys.newSettings_workplace_chooseAnIcon.tr()),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_RemoveIconButton(
|
_RemoveIconButton(
|
||||||
onTap: () => onSelected(EmojiPickerResult.none()),
|
onTap: () => onSelected(EmojiPickerResult.none()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 2),
|
const VSpace(12.0),
|
||||||
|
const Divider(height: 0.5),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: FlowyEmojiPicker(
|
||||||
children: [
|
|
||||||
FlowyEmojiPicker(
|
|
||||||
emojiPerLine: _getEmojiPerLine(context),
|
emojiPerLine: _getEmojiPerLine(context),
|
||||||
onEmojiSelected: (_, emoji) =>
|
onEmojiSelected: (_, emoji) =>
|
||||||
onSelected(EmojiPickerResult.emoji(emoji)),
|
onSelected(EmojiPickerResult.emoji(emoji)),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTabs(BuildContext context) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: TabBar(
|
|
||||||
indicatorSize: TabBarIndicatorSize.label,
|
|
||||||
isScrollable: true,
|
|
||||||
overlayColor: WidgetStatePropertyAll(
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
tabs: [
|
|
||||||
FlowyHover(
|
|
||||||
style: const HoverStyle(borderRadius: BorderRadius.zero),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.0,
|
|
||||||
vertical: 8.0,
|
|
||||||
),
|
|
||||||
child: FlowyText(LocaleKeys.emoji_emojiTab.tr()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -117,7 +85,7 @@ class FlowyIconPicker extends StatelessWidget {
|
|||||||
return 9;
|
return 9;
|
||||||
}
|
}
|
||||||
final width = MediaQuery.of(context).size.width;
|
final width = MediaQuery.of(context).size.width;
|
||||||
return width ~/ 46.0; // the size of the emoji
|
return width ~/ 40.0; // the size of the emoji
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,14 +97,14 @@ class _RemoveIconButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 28,
|
height: 24,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.small(
|
text: FlowyText.regular(
|
||||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
LocaleKeys.button_remove.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,8 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
|||||||
final String? initialRowId;
|
final String? initialRowId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewTitleBar(view: notifier.view);
|
Widget get leftBarItem =>
|
||||||
|
ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
@ -278,7 +279,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
DatabaseShareButton(key: ValueKey(view.id), view: view),
|
DatabaseShareButton(key: ValueKey(view.id), view: view),
|
||||||
const HSpace(4),
|
const HSpace(10),
|
||||||
ViewFavoriteButton(view: view),
|
ViewFavoriteButton(view: view),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
MoreViewActions(view: view, isDocument: false),
|
MoreViewActions(view: view, isDocument: false),
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/share_bloc.dart';
|
import 'package:appflowy/plugins/database/application/share_bloc.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
@ -13,6 +11,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class DatabaseShareButton extends StatelessWidget {
|
class DatabaseShareButton extends StatelessWidget {
|
||||||
@ -39,11 +38,7 @@ class DatabaseShareButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>(
|
child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>(
|
||||||
builder: (context, state) => ConstrainedBox(
|
builder: (context, state) => IntrinsicWidth(
|
||||||
constraints: const BoxConstraints.expand(
|
|
||||||
height: 30,
|
|
||||||
width: 100,
|
|
||||||
),
|
|
||||||
child: DatabaseShareActionList(view: view),
|
child: DatabaseShareActionList(view: view),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -106,6 +101,8 @@ class DatabaseShareActionListState extends State<DatabaseShareActionList> {
|
|||||||
onPointerDown: (_) => controller.show(),
|
onPointerDown: (_) => controller.show(),
|
||||||
child: RoundedTextButton(
|
child: RoundedTextButton(
|
||||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
fontSize: 14.0,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
library document_plugin;
|
library document_plugin;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
@ -22,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class DocumentPluginBuilder extends PluginBuilder {
|
class DocumentPluginBuilder extends PluginBuilder {
|
||||||
@ -130,7 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewTitleBar(view: view);
|
Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
@ -162,7 +161,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
key: ValueKey('share_button_${view.id}'),
|
key: ValueKey('share_button_${view.id}'),
|
||||||
view: view,
|
view: view,
|
||||||
),
|
),
|
||||||
const HSpace(4),
|
const HSpace(10),
|
||||||
ViewFavoriteButton(
|
ViewFavoriteButton(
|
||||||
key: ValueKey('favorite_button_${view.id}'),
|
key: ValueKey('favorite_button_${view.id}'),
|
||||||
view: view,
|
view: view,
|
||||||
|
@ -392,12 +392,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
if (widget.editorState.document.isEmpty) {
|
if (widget.editorState.document.isEmpty) {
|
||||||
return (true, Selection.collapsed(Position(path: [0])));
|
return (true, Selection.collapsed(Position(path: [0])));
|
||||||
}
|
}
|
||||||
final nodes =
|
|
||||||
widget.editorState.document.root.children.where((e) => e.delta != null);
|
|
||||||
final isAllEmpty = nodes.isNotEmpty && nodes.every((e) => e.delta!.isEmpty);
|
|
||||||
if (isAllEmpty) {
|
|
||||||
return (true, Selection.collapsed(Position(path: nodes.first.path)));
|
|
||||||
}
|
|
||||||
return const (false, null);
|
return const (false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ enum OptionDepthType {
|
|||||||
|
|
||||||
class DividerOptionAction extends CustomActionCell {
|
class DividerOptionAction extends CustomActionCell {
|
||||||
@override
|
@override
|
||||||
Widget buildWithContext(BuildContext context) {
|
Widget buildWithContext(BuildContext context, PopoverController controller) {
|
||||||
return const Divider(
|
return const Divider(
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
thickness: 1.0,
|
thickness: 1.0,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class EmojiPickerButton extends StatelessWidget {
|
class EmojiPickerButton extends StatelessWidget {
|
||||||
@ -19,6 +18,7 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
this.offset,
|
this.offset,
|
||||||
this.direction,
|
this.direction,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.showBorder = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String emoji;
|
final String emoji;
|
||||||
@ -30,6 +30,7 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
final Offset? offset;
|
final Offset? offset;
|
||||||
final PopoverDirection? direction;
|
final PopoverDirection? direction;
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final bool showBorder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -51,21 +52,27 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
onExit: () {},
|
onExit: () {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: emoji.isEmpty && defaultIcon != null
|
child: Container(
|
||||||
? FlowyButton(
|
width: 30.0,
|
||||||
useIntrinsicWidth: true,
|
height: 30.0,
|
||||||
text: defaultIcon!,
|
decoration: BoxDecoration(
|
||||||
onTap: popoverController.show,
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: showBorder
|
||||||
|
? Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
)
|
)
|
||||||
: FlowyTextButton(
|
: null,
|
||||||
emoji,
|
),
|
||||||
overflow: TextOverflow.visible,
|
child: FlowyButton(
|
||||||
fontSize: emojiSize,
|
margin: emoji.isEmpty && defaultIcon != null
|
||||||
padding: EdgeInsets.zero,
|
? EdgeInsets.zero
|
||||||
constraints: const BoxConstraints(minWidth: 35.0),
|
: const EdgeInsets.only(left: 2.0),
|
||||||
fillColor: Colors.transparent,
|
expandText: false,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
text: emoji.isEmpty && defaultIcon != null
|
||||||
onPressed: popoverController.show,
|
? defaultIcon!
|
||||||
|
: FlowyText.emoji(emoji, fontSize: emojiSize),
|
||||||
|
onTap: popoverController.show,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -300,9 +300,10 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
|||||||
: (CoverType.color, '0xffe8e0ff'),
|
: (CoverType.color, '0xffe8e0ff'),
|
||||||
),
|
),
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
leftIcon: const FlowySvg(FlowySvgs.image_s),
|
leftIcon: const FlowySvg(FlowySvgs.add_cover_s),
|
||||||
text: FlowyText.small(
|
text: FlowyText.small(
|
||||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -311,28 +312,24 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
|||||||
if (widget.hasIcon) {
|
if (widget.hasIcon) {
|
||||||
children.add(
|
children.add(
|
||||||
FlowyButton(
|
FlowyButton(
|
||||||
leftIconSize: const Size.square(18),
|
|
||||||
onTap: () => widget.onIconOrCoverChanged(icon: ""),
|
onTap: () => widget.onIconOrCoverChanged(icon: ""),
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
leftIcon: const Icon(
|
leftIcon: const FlowySvg(FlowySvgs.add_icon_s),
|
||||||
Icons.emoji_emotions_outlined,
|
iconPadding: 4.0,
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
text: FlowyText.small(
|
text: FlowyText.small(
|
||||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Widget child = FlowyButton(
|
Widget child = FlowyButton(
|
||||||
leftIconSize: const Size.square(18),
|
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
leftIcon: const Icon(
|
leftIcon: const FlowySvg(FlowySvgs.add_icon_s),
|
||||||
Icons.emoji_emotions_outlined,
|
iconPadding: 4.0,
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
text: FlowyText.small(
|
text: FlowyText.small(
|
||||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
onTap: PlatformExtension.isDesktop
|
onTap: PlatformExtension.isDesktop
|
||||||
? null
|
? null
|
||||||
|
@ -148,7 +148,7 @@ class _UnsplashImages extends StatelessWidget {
|
|||||||
type: type,
|
type: type,
|
||||||
photo: photo,
|
photo: photo,
|
||||||
onTap: () => onSelectUnsplashImage(
|
onTap: () => onSelectUnsplashImage(
|
||||||
photo.urls.regular.toString(),
|
photo.urls.full.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -41,12 +41,9 @@ class DocumentShareButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
||||||
builder: (context, state) => ConstrainedBox(
|
builder: (context, state) => SizedBox(
|
||||||
constraints: const BoxConstraints.expand(
|
height: 32.0,
|
||||||
height: 30,
|
child: IntrinsicWidth(child: ShareActionList(view: view)),
|
||||||
width: 100,
|
|
||||||
),
|
|
||||||
child: ShareActionList(view: view),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -120,7 +117,9 @@ class ShareActionListState extends State<ShareActionList> {
|
|||||||
onPointerDown: (_) => controller.show(),
|
onPointerDown: (_) => controller.show(),
|
||||||
child: RoundedTextButton(
|
child: RoundedTextButton(
|
||||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
|
fontSize: 14.0,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -32,25 +33,23 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
|||||||
_listener.start(
|
_listener.start(
|
||||||
favoritesUpdated: _onFavoritesUpdated,
|
favoritesUpdated: _onFavoritesUpdated,
|
||||||
);
|
);
|
||||||
final result = await _service.readFavorites();
|
add(const FavoriteEvent.fetchFavorites());
|
||||||
emit(
|
|
||||||
result.fold(
|
|
||||||
(view) => state.copyWith(
|
|
||||||
views: view.items,
|
|
||||||
),
|
|
||||||
(error) => state.copyWith(
|
|
||||||
views: [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
fetchFavorites: () async {
|
fetchFavorites: () async {
|
||||||
final result = await _service.readFavorites();
|
final result = await _service.readFavorites();
|
||||||
emit(
|
emit(
|
||||||
result.fold(
|
result.fold(
|
||||||
(view) => state.copyWith(
|
(favoriteViews) {
|
||||||
views: view.items,
|
final views = favoriteViews.items.map((v) => v.item).toList();
|
||||||
),
|
final pinnedViews = views.where((v) => v.isPinned).toList();
|
||||||
|
final unpinnedViews =
|
||||||
|
views.where((v) => !v.isPinned).toList();
|
||||||
|
return state.copyWith(
|
||||||
|
views: views,
|
||||||
|
pinnedViews: pinnedViews,
|
||||||
|
unpinnedViews: unpinnedViews,
|
||||||
|
);
|
||||||
|
},
|
||||||
(error) => state.copyWith(
|
(error) => state.copyWith(
|
||||||
views: [],
|
views: [],
|
||||||
),
|
),
|
||||||
@ -58,11 +57,26 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
toggle: (view) async {
|
toggle: (view) async {
|
||||||
|
if (view.isFavorite) {
|
||||||
|
await _service.unpinFavorite(view);
|
||||||
|
} else if (state.pinnedViews.length < 3) {
|
||||||
|
// pin the view if there are less than 3 pinned views
|
||||||
|
await _service.pinFavorite(view);
|
||||||
|
}
|
||||||
|
|
||||||
await _service.toggleFavorite(
|
await _service.toggleFavorite(
|
||||||
view.id,
|
view.id,
|
||||||
!view.isFavorite,
|
!view.isFavorite,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
pin: (view) async {
|
||||||
|
await _service.pinFavorite(view);
|
||||||
|
add(const FavoriteEvent.fetchFavorites());
|
||||||
|
},
|
||||||
|
unpin: (view) async {
|
||||||
|
await _service.unpinFavorite(view);
|
||||||
|
add(const FavoriteEvent.fetchFavorites());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -84,12 +98,16 @@ class FavoriteEvent with _$FavoriteEvent {
|
|||||||
const factory FavoriteEvent.initial() = Initial;
|
const factory FavoriteEvent.initial() = Initial;
|
||||||
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
|
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
|
||||||
const factory FavoriteEvent.fetchFavorites() = FetchFavorites;
|
const factory FavoriteEvent.fetchFavorites() = FetchFavorites;
|
||||||
|
const factory FavoriteEvent.pin(ViewPB view) = PinFavorite;
|
||||||
|
const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class FavoriteState with _$FavoriteState {
|
class FavoriteState with _$FavoriteState {
|
||||||
const factory FavoriteState({
|
const factory FavoriteState({
|
||||||
required List<ViewPB> views,
|
required List<ViewPB> views,
|
||||||
|
@Default([]) List<ViewPB> pinnedViews,
|
||||||
|
@Default([]) List<ViewPB> unpinnedViews,
|
||||||
}) = _FavoriteState;
|
}) = _FavoriteState;
|
||||||
|
|
||||||
factory FavoriteState.initial() => const FavoriteState(
|
factory FavoriteState.initial() => const FavoriteState(
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class FavoriteService {
|
class FavoriteService {
|
||||||
Future<FlowyResult<RepeatedViewPB, FlowyError>> readFavorites() {
|
Future<FlowyResult<RepeatedFavoriteViewPB, FlowyError>> readFavorites() {
|
||||||
return FolderEventReadFavorites().send();
|
return FolderEventReadFavorites().send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,4 +20,33 @@ class FavoriteService {
|
|||||||
final id = RepeatedViewIdPB.create()..items.add(viewId);
|
final id = RepeatedViewIdPB.create()..items.add(viewId);
|
||||||
return FolderEventToggleFavorite(id).send();
|
return FolderEventToggleFavorite(id).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FlowyResult<void, FlowyError>> pinFavorite(ViewPB view) async {
|
||||||
|
return pinOrUnpinFavorite(view, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FlowyResult<void, FlowyError>> unpinFavorite(ViewPB view) async {
|
||||||
|
return pinOrUnpinFavorite(view, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FlowyResult<void, FlowyError>> pinOrUnpinFavorite(
|
||||||
|
ViewPB view,
|
||||||
|
bool isPinned,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {};
|
||||||
|
final merged = mergeMaps(
|
||||||
|
current,
|
||||||
|
<String, dynamic>{ViewExtKeys.isPinnedKey: isPinned},
|
||||||
|
);
|
||||||
|
await ViewBackendService.updateView(
|
||||||
|
viewId: view.id,
|
||||||
|
extra: jsonEncode(merged),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlowyResult.success(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ abstract class BaseAppearance {
|
|||||||
double? letterSpacing,
|
double? letterSpacing,
|
||||||
double? lineHeight,
|
double? lineHeight,
|
||||||
}) {
|
}) {
|
||||||
fontSize = fontSize ?? FontSizes.s12;
|
fontSize = fontSize ?? FontSizes.s14;
|
||||||
fontWeight = fontWeight ??
|
fontWeight = fontWeight ??
|
||||||
(PlatformExtension.isDesktopOrWeb ? FontWeight.w500 : FontWeight.w400);
|
(PlatformExtension.isDesktopOrWeb ? FontWeight.w500 : FontWeight.w400);
|
||||||
letterSpacing = fontSize * (letterSpacing ?? 0.005);
|
letterSpacing = fontSize * (letterSpacing ?? 0.005);
|
||||||
|
@ -9,18 +9,18 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
|
|
||||||
part 'folder_bloc.freezed.dart';
|
part 'folder_bloc.freezed.dart';
|
||||||
|
|
||||||
enum FolderCategoryType {
|
enum FolderSpaceType {
|
||||||
favorite,
|
favorite,
|
||||||
private,
|
private,
|
||||||
public;
|
public;
|
||||||
|
|
||||||
ViewSectionPB get toViewSectionPB {
|
ViewSectionPB get toViewSectionPB {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case FolderCategoryType.private:
|
case FolderSpaceType.private:
|
||||||
return ViewSectionPB.Private;
|
return ViewSectionPB.Private;
|
||||||
case FolderCategoryType.public:
|
case FolderSpaceType.public:
|
||||||
return ViewSectionPB.Public;
|
return ViewSectionPB.Public;
|
||||||
case FolderCategoryType.favorite:
|
case FolderSpaceType.favorite:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ enum FolderCategoryType {
|
|||||||
|
|
||||||
class FolderBloc extends Bloc<FolderEvent, FolderState> {
|
class FolderBloc extends Bloc<FolderEvent, FolderState> {
|
||||||
FolderBloc({
|
FolderBloc({
|
||||||
required FolderCategoryType type,
|
required FolderSpaceType type,
|
||||||
}) : super(FolderState.initial(type)) {
|
}) : super(FolderState.initial(type)) {
|
||||||
on<FolderEvent>((event, emit) async {
|
on<FolderEvent>((event, emit) async {
|
||||||
await event.map(
|
await event.map(
|
||||||
@ -84,12 +84,12 @@ class FolderEvent with _$FolderEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class FolderState with _$FolderState {
|
class FolderState with _$FolderState {
|
||||||
const factory FolderState({
|
const factory FolderState({
|
||||||
required FolderCategoryType type,
|
required FolderSpaceType type,
|
||||||
required bool isExpanded,
|
required bool isExpanded,
|
||||||
}) = _FolderState;
|
}) = _FolderState;
|
||||||
|
|
||||||
factory FolderState.initial(
|
factory FolderState.initial(
|
||||||
FolderCategoryType type,
|
FolderSpaceType type,
|
||||||
) =>
|
) =>
|
||||||
FolderState(
|
FolderState(
|
||||||
type: type,
|
type: type,
|
||||||
|
@ -75,7 +75,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||||||
);
|
);
|
||||||
final isExpanded = await _getViewIsExpanded(view);
|
final isExpanded = await _getViewIsExpanded(view);
|
||||||
emit(state.copyWith(isExpanded: isExpanded));
|
emit(state.copyWith(isExpanded: isExpanded));
|
||||||
await _loadViewsWhenExpanded(emit, isExpanded);
|
await _loadChildViews(emit);
|
||||||
},
|
},
|
||||||
setIsEditing: (e) {
|
setIsEditing: (e) {
|
||||||
emit(state.copyWith(isEditing: e.isEditing));
|
emit(state.copyWith(isEditing: e.isEditing));
|
||||||
@ -222,6 +222,12 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||||||
viewIcon: value.icon ?? '',
|
viewIcon: value.icon ?? '',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
collapseAllPages: (value) async {
|
||||||
|
for (final childView in view.childViews) {
|
||||||
|
await _setViewIsExpanded(childView, false);
|
||||||
|
}
|
||||||
|
add(const ViewEvent.setIsExpanded(false));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -270,6 +276,33 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadChildViews(
|
||||||
|
Emitter<ViewState> emit,
|
||||||
|
) async {
|
||||||
|
final viewsOrFailed =
|
||||||
|
await ViewBackendService.getChildViews(viewId: state.view.id);
|
||||||
|
|
||||||
|
viewsOrFailed.fold(
|
||||||
|
(childViews) {
|
||||||
|
state.view.freeze();
|
||||||
|
final viewWithChildViews = state.view.rebuild((b) {
|
||||||
|
b.childViews.clear();
|
||||||
|
b.childViews.addAll(childViews);
|
||||||
|
});
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
view: viewWithChildViews,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(error) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
successOrFailure: FlowyResult.failure(error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {
|
Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {
|
||||||
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
|
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
|
||||||
final Map map;
|
final Map map;
|
||||||
@ -388,6 +421,7 @@ class ViewEvent with _$ViewEvent {
|
|||||||
bool isPublic,
|
bool isPublic,
|
||||||
) = UpdateViewVisibility;
|
) = UpdateViewVisibility;
|
||||||
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
|
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
|
||||||
|
const factory ViewEvent.collapseAllPages() = CollapseAllPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -32,6 +32,9 @@ class ViewExtKeys {
|
|||||||
static String coverKey = 'cover';
|
static String coverKey = 'cover';
|
||||||
static String coverTypeKey = 'type';
|
static String coverTypeKey = 'type';
|
||||||
static String coverValueKey = 'value';
|
static String coverValueKey = 'value';
|
||||||
|
|
||||||
|
// is pinned
|
||||||
|
static String isPinnedKey = 'is_pinned';
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewExtension on ViewPB {
|
extension ViewExtension on ViewPB {
|
||||||
@ -96,6 +99,16 @@ extension ViewExtension on ViewPB {
|
|||||||
|
|
||||||
FlowySvgData get iconData => layout.icon;
|
FlowySvgData get iconData => layout.icon;
|
||||||
|
|
||||||
|
bool get isPinned {
|
||||||
|
try {
|
||||||
|
final ext = jsonDecode(extra);
|
||||||
|
final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false;
|
||||||
|
return isPinned;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PageStyleCover? get cover {
|
PageStyleCover? get cover {
|
||||||
if (layout != ViewLayoutPB.Document) {
|
if (layout != ViewLayoutPB.Document) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'view_title_bar_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
|
||||||
|
ViewTitleBarBloc({
|
||||||
|
required this.view,
|
||||||
|
}) : super(ViewTitleBarState.initial()) {
|
||||||
|
on<ViewTitleBarEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
add(const ViewTitleBarEvent.reload());
|
||||||
|
},
|
||||||
|
reload: () async {
|
||||||
|
final List<ViewPB> ancestors =
|
||||||
|
await ViewBackendService.getViewAncestors(view.id).fold(
|
||||||
|
(s) => s.items,
|
||||||
|
(f) => [],
|
||||||
|
);
|
||||||
|
emit(state.copyWith(ancestors: ancestors));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ViewTitleBarEvent with _$ViewTitleBarEvent {
|
||||||
|
const factory ViewTitleBarEvent.initial() = Initial;
|
||||||
|
const factory ViewTitleBarEvent.reload() = Reload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ViewTitleBarState with _$ViewTitleBarState {
|
||||||
|
const factory ViewTitleBarState({
|
||||||
|
required List<ViewPB> ancestors,
|
||||||
|
}) = _ViewTitleBarState;
|
||||||
|
|
||||||
|
factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []);
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'view_title_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> {
|
||||||
|
ViewTitleBloc({
|
||||||
|
required this.view,
|
||||||
|
}) : viewListener = ViewListener(viewId: view.id),
|
||||||
|
super(ViewTitleState.initial()) {
|
||||||
|
on<ViewTitleEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
name: view.name,
|
||||||
|
icon: view.icon.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
viewListener.start(
|
||||||
|
onViewUpdated: (view) {
|
||||||
|
add(
|
||||||
|
ViewTitleEvent.updateNameOrIcon(
|
||||||
|
view.name,
|
||||||
|
view.icon.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateNameOrIcon: (name, icon) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
name: name,
|
||||||
|
icon: icon,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final ViewListener viewListener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
viewListener.stop();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ViewTitleEvent with _$ViewTitleEvent {
|
||||||
|
const factory ViewTitleEvent.initial() = Initial;
|
||||||
|
const factory ViewTitleEvent.updateNameOrIcon(String name, String icon) =
|
||||||
|
UpdateNameOrIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ViewTitleState with _$ViewTitleState {
|
||||||
|
const factory ViewTitleState({
|
||||||
|
required String name,
|
||||||
|
required String icon,
|
||||||
|
}) = _ViewTitleState;
|
||||||
|
|
||||||
|
factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: '');
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
class HomeSizes {
|
class HomeSizes {
|
||||||
static const double menuAddButtonHeight = 60;
|
static const double menuAddButtonHeight = 60;
|
||||||
static const double topBarHeight = 60;
|
static const double topBarHeight = 44;
|
||||||
static const double editPanelTopBarHeight = 60;
|
static const double editPanelTopBarHeight = 60;
|
||||||
static const double editPanelWidth = 400;
|
static const double editPanelWidth = 400;
|
||||||
static const double tabBarHeigth = 40;
|
static const double tabBarHeight = 40;
|
||||||
static const double tabBarWidth = 200;
|
static const double tabBarWidth = 200;
|
||||||
|
static const double workspaceSectionHeight = 32;
|
||||||
|
static const double searchSectionHeight = 30;
|
||||||
|
static const double newPageSectionHeight = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeInsets {
|
class HomeInsets {
|
||||||
static const double topBarTitlePadding = 12;
|
static const double topBarTitleHorizontalPadding = 12;
|
||||||
|
static const double topBarTitleVerticalPadding = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeSpaceViewSizes {
|
||||||
|
static const double leftPadding = 16.0;
|
||||||
|
static const double viewHeight = 30.0;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/frameless_window.dart';
|
import 'package:appflowy/core/frameless_window.dart';
|
||||||
import 'package:appflowy/plugins/blank/blank.dart';
|
import 'package:appflowy/plugins/blank/blank.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
@ -13,6 +10,8 @@ import 'package:appflowy/workspace/presentation/home/toast.dart';
|
|||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:time/time.dart';
|
import 'package:time/time.dart';
|
||||||
@ -275,14 +274,12 @@ class HomeTopBar extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
|
||||||
),
|
),
|
||||||
),
|
height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding,
|
||||||
height: HomeSizes.topBarHeight,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: HomeInsets.topBarTitlePadding,
|
horizontal: HomeInsets.topBarTitleHorizontalPadding,
|
||||||
|
vertical: HomeInsets.topBarTitleVerticalPadding,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -6,7 +6,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -0,0 +1,183 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
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/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';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class FavoriteFolder extends StatefulWidget {
|
||||||
|
const FavoriteFolder({
|
||||||
|
super.key,
|
||||||
|
required this.views,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<ViewPB> views;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FavoriteFolder> createState() => _FavoriteFolderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteFolderState extends State<FavoriteFolder> {
|
||||||
|
final isHovered = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
isHovered.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.views.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlocProvider<FolderBloc>(
|
||||||
|
create: (context) => FolderBloc(type: FolderSpaceType.favorite)
|
||||||
|
..add(const FolderEvent.initial()),
|
||||||
|
child: BlocBuilder<FolderBloc, FolderState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => isHovered.value = true,
|
||||||
|
onExit: (_) => isHovered.value = false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FavoriteHeader(
|
||||||
|
onPressed: () => context
|
||||||
|
.read<FolderBloc>()
|
||||||
|
.add(const FolderEvent.expandOrUnExpand()),
|
||||||
|
),
|
||||||
|
// pages
|
||||||
|
..._buildViews(context, state, isHovered),
|
||||||
|
if (state.isExpanded) ...[
|
||||||
|
// more button
|
||||||
|
const VSpace(2),
|
||||||
|
const FavoriteMoreButton(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Widget> _buildViews(
|
||||||
|
BuildContext context,
|
||||||
|
FolderState state,
|
||||||
|
ValueNotifier<bool> isHovered,
|
||||||
|
) {
|
||||||
|
if (!state.isExpanded) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.read<FavoriteBloc>().state.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,
|
||||||
|
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,
|
||||||
|
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 {
|
||||||
|
const FavoriteHeader({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyButton(
|
||||||
|
onTap: onPressed,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
|
||||||
|
leftIcon: const FlowySvg(
|
||||||
|
FlowySvgs.favorite_header_icon_s,
|
||||||
|
blendMode: null,
|
||||||
|
),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText.regular(LocaleKeys.sideBar_favorites.tr()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavoriteMoreButton extends StatelessWidget {
|
||||||
|
const FavoriteMoreButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final favoriteBloc = context.watch<FavoriteBloc>();
|
||||||
|
final unpinnedViews = favoriteBloc.state.unpinnedViews;
|
||||||
|
// only show the more button if there are unpinned views
|
||||||
|
if (unpinnedViews.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
const minWidth = 260.0;
|
||||||
|
return AppFlowyPopover(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: minWidth,
|
||||||
|
),
|
||||||
|
decoration: FlowyDecoration.decoration(
|
||||||
|
Theme.of(context).cardColor,
|
||||||
|
Theme.of(context).colorScheme.shadow,
|
||||||
|
borderRadius: 10.0,
|
||||||
|
),
|
||||||
|
popupBuilder: (_) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: favoriteBloc,
|
||||||
|
child: const FavoriteMenu(minWidth: minWidth),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: FlowyButton(
|
||||||
|
onTap: () {},
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0),
|
||||||
|
leftIcon: const FlowySvg(
|
||||||
|
FlowySvgs.workspace_three_dots_s,
|
||||||
|
),
|
||||||
|
text: FlowyText.regular(LocaleKeys.button_more.tr()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
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/favorites/favorite_menu_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
const double _kHorizontalPadding = 10.0;
|
||||||
|
const double _kVerticalPadding = 10.0;
|
||||||
|
|
||||||
|
class FavoriteMenu extends StatelessWidget {
|
||||||
|
const FavoriteMenu({super.key, required this.minWidth});
|
||||||
|
|
||||||
|
final double minWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: _kHorizontalPadding,
|
||||||
|
right: _kHorizontalPadding,
|
||||||
|
top: _kVerticalPadding,
|
||||||
|
bottom: _kVerticalPadding,
|
||||||
|
),
|
||||||
|
child: BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()),
|
||||||
|
child: BlocBuilder<FavoriteMenuBloc, FavoriteMenuState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.views.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const VSpace(4),
|
||||||
|
_FavoriteSearchField(
|
||||||
|
width: minWidth - 2 * _kHorizontalPadding,
|
||||||
|
onSearch: (context, text) {
|
||||||
|
context
|
||||||
|
.read<FavoriteMenuBloc>()
|
||||||
|
.add(FavoriteMenuEvent.search(text));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const VSpace(12),
|
||||||
|
_buildViews(context, state),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildViews(BuildContext context, FavoriteMenuState state) {
|
||||||
|
return Container(
|
||||||
|
width: minWidth - 2 * _kHorizontalPadding,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxHeight: 300,
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
..._buildGroups(
|
||||||
|
context,
|
||||||
|
state.todayViews,
|
||||||
|
LocaleKeys.sideBar_today.tr(),
|
||||||
|
),
|
||||||
|
..._buildGroups(
|
||||||
|
context,
|
||||||
|
state.thisWeekViews,
|
||||||
|
LocaleKeys.sideBar_thisWeek.tr(),
|
||||||
|
),
|
||||||
|
..._buildGroups(
|
||||||
|
context,
|
||||||
|
state.otherViews,
|
||||||
|
LocaleKeys.sideBar_others.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildGroups(
|
||||||
|
BuildContext context,
|
||||||
|
List<ViewPB> views,
|
||||||
|
String title,
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
if (views.isNotEmpty) ...[
|
||||||
|
SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: FlowyText(
|
||||||
|
title,
|
||||||
|
fontSize: 12.0,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VSpace(2),
|
||||||
|
_buildGroupedViews(context, views),
|
||||||
|
const VSpace(8),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const VSpace(8),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroupedViews(BuildContext context, List<ViewPB> views) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: views
|
||||||
|
.map(
|
||||||
|
(e) => ViewItem(
|
||||||
|
key: ValueKey(e.id),
|
||||||
|
view: e,
|
||||||
|
spaceType: FolderSpaceType.favorite,
|
||||||
|
level: 0,
|
||||||
|
onSelected: (view, _) {},
|
||||||
|
isFeedback: false,
|
||||||
|
isDraggable: false,
|
||||||
|
shouldRenderChildren: false,
|
||||||
|
leftIconBuilder: (_, __) => const HSpace(4.0),
|
||||||
|
rightIconsBuilder: (_, view) => [
|
||||||
|
FavoriteMoreActions(view: view),
|
||||||
|
const HSpace(6.0),
|
||||||
|
FavoritePinAction(view: view),
|
||||||
|
const HSpace(4.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteSearchField extends StatelessWidget {
|
||||||
|
const _FavoriteSearchField({
|
||||||
|
required this.width,
|
||||||
|
required this.onSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
final void Function(BuildContext context, String text) onSearch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 30,
|
||||||
|
width: width,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: const BorderSide(
|
||||||
|
width: 1.20,
|
||||||
|
strokeAlign: BorderSide.strokeAlignOutside,
|
||||||
|
color: Color(0xFF00BCF0),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CupertinoSearchTextField(
|
||||||
|
onChanged: (text) => onSearch(context, text),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
placeholder: LocaleKeys.search_label.tr(),
|
||||||
|
prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
|
||||||
|
prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0),
|
||||||
|
suffixIcon: const Icon(Icons.close),
|
||||||
|
suffixInsets: const EdgeInsets.only(right: 8.0),
|
||||||
|
itemSize: 16.0,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'favorite_menu_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class FavoriteMenuBloc extends Bloc<FavoriteMenuEvent, FavoriteMenuState> {
|
||||||
|
FavoriteMenuBloc() : super(FavoriteMenuState.initial()) {
|
||||||
|
on<FavoriteMenuEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
final favoriteViews = await _service.readFavorites();
|
||||||
|
List<ViewPB> views = [];
|
||||||
|
List<ViewPB> todayViews = [];
|
||||||
|
List<ViewPB> thisWeekViews = [];
|
||||||
|
List<ViewPB> otherViews = [];
|
||||||
|
|
||||||
|
favoriteViews.onSuccess((s) {
|
||||||
|
_source = s;
|
||||||
|
(views, todayViews, thisWeekViews, otherViews) = _getViews(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
views: views,
|
||||||
|
queriedViews: views,
|
||||||
|
todayViews: todayViews,
|
||||||
|
thisWeekViews: thisWeekViews,
|
||||||
|
otherViews: otherViews,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
search: (query) async {
|
||||||
|
if (_source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var (views, todayViews, thisWeekViews, otherViews) =
|
||||||
|
_getViews(_source!);
|
||||||
|
var queriedViews = views;
|
||||||
|
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
queriedViews = _filter(views, query);
|
||||||
|
todayViews = _filter(state.todayViews, query);
|
||||||
|
thisWeekViews = _filter(state.thisWeekViews, query);
|
||||||
|
otherViews = _filter(state.otherViews, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
views: views,
|
||||||
|
queriedViews: queriedViews,
|
||||||
|
todayViews: todayViews,
|
||||||
|
thisWeekViews: thisWeekViews,
|
||||||
|
otherViews: otherViews,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final FavoriteService _service = FavoriteService();
|
||||||
|
RepeatedFavoriteViewPB? _source;
|
||||||
|
|
||||||
|
List<ViewPB> _filter(List<ViewPB> views, String query) {
|
||||||
|
return views
|
||||||
|
.where(
|
||||||
|
(view) => view.name.toLowerCase().contains(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// all, today, last week, other
|
||||||
|
(List<ViewPB>, List<ViewPB>, List<ViewPB>, List<ViewPB>) _getViews(
|
||||||
|
RepeatedFavoriteViewPB source,
|
||||||
|
) {
|
||||||
|
final List<ViewPB> views =
|
||||||
|
source.items.map((v) => v.item).where((e) => !e.isPinned).toList();
|
||||||
|
final List<ViewPB> todayViews = [];
|
||||||
|
final List<ViewPB> thisWeekViews = [];
|
||||||
|
final List<ViewPB> otherViews = [];
|
||||||
|
for (final favoriteView in source.items) {
|
||||||
|
final view = favoriteView.item;
|
||||||
|
if (view.isPinned) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
favoriteView.timestamp.toInt() * 1000,
|
||||||
|
);
|
||||||
|
final diff = DateTime.now().difference(date).inDays;
|
||||||
|
if (diff == 0) {
|
||||||
|
todayViews.add(view);
|
||||||
|
} else if (diff < 7) {
|
||||||
|
thisWeekViews.add(view);
|
||||||
|
} else {
|
||||||
|
otherViews.add(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (views, todayViews, thisWeekViews, otherViews);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class FavoriteMenuEvent with _$FavoriteMenuEvent {
|
||||||
|
const factory FavoriteMenuEvent.initial() = Initial;
|
||||||
|
const factory FavoriteMenuEvent.search(String query) = Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class FavoriteMenuState with _$FavoriteMenuState {
|
||||||
|
const factory FavoriteMenuState({
|
||||||
|
@Default([]) List<ViewPB> views,
|
||||||
|
@Default([]) List<ViewPB> queriedViews,
|
||||||
|
@Default([]) List<ViewPB> todayViews,
|
||||||
|
@Default([]) List<ViewPB> thisWeekViews,
|
||||||
|
@Default([]) List<ViewPB> otherViews,
|
||||||
|
}) = _FavoriteMenuState;
|
||||||
|
|
||||||
|
factory FavoriteMenuState.initial() => const FavoriteMenuState();
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
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/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class FavoriteMoreActions extends StatelessWidget {
|
||||||
|
const FavoriteMoreActions({super.key, required this.view});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyTooltip(
|
||||||
|
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
||||||
|
child: ViewMoreActionButton(
|
||||||
|
view: view,
|
||||||
|
spaceType: FolderSpaceType.favorite,
|
||||||
|
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(view));
|
||||||
|
PopoverContainer.maybeOf(context)?.closeAll();
|
||||||
|
break;
|
||||||
|
case ViewMoreActionType.rename:
|
||||||
|
NavigatorTextFieldDialog(
|
||||||
|
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||||
|
autoSelectAllText: true,
|
||||||
|
value: view.name,
|
||||||
|
maxLength: 256,
|
||||||
|
onConfirm: (newValue, _) {
|
||||||
|
// can not use bloc here because it has been disposed.
|
||||||
|
ViewBackendService.updateView(
|
||||||
|
viewId: view.id,
|
||||||
|
name: newValue,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).show(context);
|
||||||
|
PopoverContainer.maybeOf(context)?.closeAll();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ViewMoreActionType.openInNewTab:
|
||||||
|
context.read<TabsBloc>().openTab(view);
|
||||||
|
break;
|
||||||
|
case ViewMoreActionType.delete:
|
||||||
|
case ViewMoreActionType.duplicate:
|
||||||
|
default:
|
||||||
|
throw UnsupportedError('$action is not supported');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class FavoritePinAction extends StatelessWidget {
|
||||||
|
const FavoritePinAction({super.key, required this.view});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tooltip = view.isPinned
|
||||||
|
? LocaleKeys.favorite_removeFromSidebar.tr()
|
||||||
|
: LocaleKeys.favorite_addToSidebar.tr();
|
||||||
|
final icon = FlowySvg(
|
||||||
|
view.isPinned
|
||||||
|
? FlowySvgs.favorite_section_pin_s
|
||||||
|
: FlowySvgs.favorite_section_unpin_s,
|
||||||
|
);
|
||||||
|
return FlowyTooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: FlowyIconButton(
|
||||||
|
width: 24,
|
||||||
|
icon: icon,
|
||||||
|
onPressed: () {
|
||||||
|
PopoverContainer.maybeOf(context)?.closeAll();
|
||||||
|
|
||||||
|
view.isPinned
|
||||||
|
? context.read<FavoriteBloc>().add(FavoriteEvent.unpin(view))
|
||||||
|
: context.read<FavoriteBloc>().add(FavoriteEvent.pin(view));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'favorite_pin_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class FavoritePinBloc extends Bloc<FavoritePinEvent, FavoritePinState> {
|
||||||
|
FavoritePinBloc() : super(FavoritePinState.initial()) {
|
||||||
|
on<FavoritePinEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
final List<ViewPB> views = await _service
|
||||||
|
.readFavorites()
|
||||||
|
.fold((s) => s.items.map((v) => v.item).toList(), (f) => []);
|
||||||
|
emit(state.copyWith(views: views, queriedViews: views));
|
||||||
|
},
|
||||||
|
search: (query) async {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
emit(state.copyWith(queriedViews: state.views));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final queriedViews = state.views
|
||||||
|
.where(
|
||||||
|
(view) =>
|
||||||
|
view.name.toLowerCase().contains(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
emit(state.copyWith(queriedViews: queriedViews));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final FavoriteService _service = FavoriteService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class FavoritePinEvent with _$FavoritePinEvent {
|
||||||
|
const factory FavoritePinEvent.initial() = Initial;
|
||||||
|
const factory FavoritePinEvent.search(String query) = Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class FavoritePinState with _$FavoritePinState {
|
||||||
|
const factory FavoritePinState({
|
||||||
|
@Default([]) List<ViewPB> views,
|
||||||
|
@Default([]) List<ViewPB> queriedViews,
|
||||||
|
@Default([]) List<List<ViewPB>> todayViews,
|
||||||
|
@Default([]) List<List<ViewPB>> lastWeekViews,
|
||||||
|
@Default([]) List<List<ViewPB>> otherViews,
|
||||||
|
}) = _FavoritePinState;
|
||||||
|
|
||||||
|
factory FavoritePinState.initial() => const FavoritePinState();
|
||||||
|
}
|
@ -1,115 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.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,
|
|
||||||
isFeedback: false,
|
|
||||||
view: view,
|
|
||||||
level: 0,
|
|
||||||
onSelected: (view, _) {
|
|
||||||
if (HardwareKeyboard.instance.isControlPressed) {
|
|
||||||
context.read<TabsBloc>().openTab(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<TabsBloc>().openPlugin(view);
|
|
||||||
},
|
|
||||||
onTertiarySelected: (view, _) =>
|
|
||||||
context.read<TabsBloc>().openTab(view),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
children: [
|
|
||||||
FlowyTextButton(
|
|
||||||
LocaleKeys.sideBar_favorites.tr(),
|
|
||||||
fontColor: AFThemeExtension.of(context).textColor,
|
|
||||||
fontHoverColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: LocaleKeys.sideBar_clickToHideFavorites.tr(),
|
|
||||||
constraints: const BoxConstraints(maxHeight: iconSize),
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
onPressed: widget.onPressed,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class FolderHeader extends StatefulWidget {
|
class FolderHeader extends StatefulWidget {
|
||||||
const FolderHeader({
|
const FolderHeader({
|
||||||
@ -25,42 +24,34 @@ class FolderHeader extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FolderHeaderState extends State<FolderHeader> {
|
class _FolderHeaderState extends State<FolderHeader> {
|
||||||
bool onHover = false;
|
final isHovered = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
isHovered.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const iconSize = 26.0;
|
|
||||||
const textPadding = 4.0;
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (event) => setState(() => onHover = true),
|
onEnter: (_) => isHovered.value = true,
|
||||||
onExit: (event) => setState(() => onHover = false),
|
onExit: (_) => isHovered.value = false,
|
||||||
child: Row(
|
child: FlowyButton(
|
||||||
children: [
|
onTap: widget.onPressed,
|
||||||
FlowyTextButton(
|
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
widget.title,
|
rightIcon: ValueListenableBuilder(
|
||||||
tooltip: widget.expandButtonTooltip,
|
valueListenable: isHovered,
|
||||||
constraints: const BoxConstraints(
|
builder: (context, onHover, child) =>
|
||||||
minHeight: iconSize + textPadding * 2,
|
Opacity(opacity: onHover ? 1 : 0, child: child),
|
||||||
),
|
child: FlowyIconButton(
|
||||||
fontColor: AFThemeExtension.of(context).textColor,
|
|
||||||
fontHoverColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
padding: const EdgeInsets.all(textPadding),
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
onPressed: widget.onPressed,
|
|
||||||
),
|
|
||||||
if (onHover) ...[
|
|
||||||
const Spacer(),
|
|
||||||
FlowyIconButton(
|
|
||||||
tooltipText: widget.addButtonTooltip,
|
tooltipText: widget.addButtonTooltip,
|
||||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
icon: const FlowySvg(FlowySvgs.view_item_add_s),
|
||||||
iconPadding: const EdgeInsets.all(2),
|
|
||||||
height: iconSize,
|
|
||||||
width: iconSize,
|
|
||||||
icon: const FlowySvg(FlowySvgs.add_s),
|
|
||||||
onPressed: widget.onAdded,
|
onPressed: widget.onAdded,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText(widget.title),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,22 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_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/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SectionFolder extends StatelessWidget {
|
class SectionFolder extends StatefulWidget {
|
||||||
const SectionFolder({
|
const SectionFolder({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
required this.views,
|
required this.views,
|
||||||
this.isHoverEnabled = true,
|
this.isHoverEnabled = true,
|
||||||
required this.expandButtonTooltip,
|
required this.expandButtonTooltip,
|
||||||
@ -24,16 +26,32 @@ class SectionFolder extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
final List<ViewPB> views;
|
final List<ViewPB> views;
|
||||||
final bool isHoverEnabled;
|
final bool isHoverEnabled;
|
||||||
final String expandButtonTooltip;
|
final String expandButtonTooltip;
|
||||||
final String addButtonTooltip;
|
final String addButtonTooltip;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SectionFolder> createState() => _SectionFolderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionFolderState extends State<SectionFolder> {
|
||||||
|
final ValueNotifier<bool> isHovered = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
isHovered.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<FolderBloc>(
|
return MouseRegion(
|
||||||
create: (context) => FolderBloc(type: categoryType)
|
onEnter: (_) => isHovered.value = true,
|
||||||
|
onExit: (_) => isHovered.value = false,
|
||||||
|
child: BlocProvider<FolderBloc>(
|
||||||
|
create: (context) => FolderBloc(type: widget.spaceType)
|
||||||
..add(
|
..add(
|
||||||
const FolderEvent.initial(),
|
const FolderEvent.initial(),
|
||||||
),
|
),
|
||||||
@ -41,13 +59,27 @@ class SectionFolder extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
FolderHeader(
|
_buildHeader(context),
|
||||||
title: title,
|
// Pages
|
||||||
expandButtonTooltip: expandButtonTooltip,
|
const VSpace(4.0),
|
||||||
addButtonTooltip: addButtonTooltip,
|
..._buildViews(context, state, isHovered),
|
||||||
onPressed: () => context
|
// Add a placeholder if there are no views
|
||||||
.read<FolderBloc>()
|
_buildDraggablePlaceholder(context),
|
||||||
.add(const FolderEvent.expandOrUnExpand()),
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return FolderHeader(
|
||||||
|
title: widget.title,
|
||||||
|
expandButtonTooltip: widget.expandButtonTooltip,
|
||||||
|
addButtonTooltip: widget.addButtonTooltip,
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<FolderBloc>().add(const FolderEvent.expandOrUnExpand()),
|
||||||
onAdded: () {
|
onAdded: () {
|
||||||
createViewAndShowRenameDialogIfNeeded(
|
createViewAndShowRenameDialogIfNeeded(
|
||||||
context,
|
context,
|
||||||
@ -58,7 +90,7 @@ class SectionFolder extends StatelessWidget {
|
|||||||
SidebarSectionsEvent.createRootViewInSection(
|
SidebarSectionsEvent.createRootViewInSection(
|
||||||
name: viewName,
|
name: viewName,
|
||||||
index: 0,
|
index: 0,
|
||||||
viewSection: categoryType.toViewSectionPB,
|
viewSection: widget.spaceType.toViewSectionPB,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -71,34 +103,48 @@ class SectionFolder extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
if (state.isExpanded)
|
}
|
||||||
...views.map(
|
|
||||||
|
Iterable<Widget> _buildViews(
|
||||||
|
BuildContext context,
|
||||||
|
FolderState state,
|
||||||
|
ValueNotifier<bool> isHovered,
|
||||||
|
) {
|
||||||
|
if (!state.isExpanded) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.views.map(
|
||||||
(view) => ViewItem(
|
(view) => ViewItem(
|
||||||
key: ValueKey(
|
key: ValueKey('${widget.spaceType.name} ${view.id}'),
|
||||||
'${categoryType.name} ${view.id}',
|
spaceType: widget.spaceType,
|
||||||
),
|
isFirstChild: view.id == widget.views.first.id,
|
||||||
categoryType: categoryType,
|
|
||||||
isFirstChild: view.id == views.first.id,
|
|
||||||
view: view,
|
view: view,
|
||||||
level: 0,
|
level: 0,
|
||||||
leftPadding: 16,
|
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||||
isFeedback: false,
|
isFeedback: false,
|
||||||
onSelected: (view, viewContext) {
|
isHovered: isHovered,
|
||||||
|
onSelected: (viewContext, view) {
|
||||||
if (HardwareKeyboard.instance.isControlPressed) {
|
if (HardwareKeyboard.instance.isControlPressed) {
|
||||||
context.read<TabsBloc>().openTab(view);
|
context.read<TabsBloc>().openTab(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.read<TabsBloc>().openPlugin(view);
|
context.read<TabsBloc>().openPlugin(view);
|
||||||
},
|
},
|
||||||
onTertiarySelected: (view, viewContext) =>
|
onTertiarySelected: (viewContext, view) =>
|
||||||
context.read<TabsBloc>().openTab(view),
|
context.read<TabsBloc>().openTab(view),
|
||||||
isHoverEnabled: isHoverEnabled,
|
isHoverEnabled: widget.isHoverEnabled,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
if (views.isEmpty)
|
}
|
||||||
ViewItem(
|
|
||||||
categoryType: categoryType,
|
Widget _buildDraggablePlaceholder(BuildContext context) {
|
||||||
|
if (widget.views.isNotEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return ViewItem(
|
||||||
|
spaceType: widget.spaceType,
|
||||||
view: ViewPB(
|
view: ViewPB(
|
||||||
parentViewId: context
|
parentViewId: context
|
||||||
.read<UserWorkspaceBloc>()
|
.read<UserWorkspaceBloc>()
|
||||||
@ -108,17 +154,12 @@ class SectionFolder extends StatelessWidget {
|
|||||||
'',
|
'',
|
||||||
),
|
),
|
||||||
level: 0,
|
level: 0,
|
||||||
leftPadding: 16,
|
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||||
isFeedback: false,
|
isFeedback: false,
|
||||||
onSelected: (_, __) {},
|
onSelected: (_, __) {},
|
||||||
onTertiarySelected: (_, __) {},
|
onTertiarySelected: (_, __) {},
|
||||||
isHoverEnabled: isHoverEnabled,
|
isHoverEnabled: widget.isHoverEnabled,
|
||||||
isPlaceholder: true,
|
isPlaceholder: true,
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SidebarFooter extends StatelessWidget {
|
||||||
|
const SidebarFooter({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: SidebarTrashButton()),
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
child: VerticalDivider(width: 1, color: Color(0x141F2329)),
|
||||||
|
),
|
||||||
|
Expanded(child: SidebarWidgetButton()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SidebarTrashButton extends StatelessWidget {
|
||||||
|
const SidebarTrashButton({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: getIt<MenuSharedState>().notifier,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
getIt<MenuSharedState>().latestOpenView = null;
|
||||||
|
getIt<TabsBloc>().add(
|
||||||
|
TabsEvent.openPlugin(
|
||||||
|
plugin: makePlugin(pluginType: PluginType.trash),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const FlowySvg(FlowySvgs.sidebar_footer_trash_s),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SidebarWidgetButton extends StatelessWidget {
|
||||||
|
const SidebarWidgetButton({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: const FlowySvg(FlowySvgs.sidebar_footer_widget_s),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -62,22 +63,33 @@ class SidebarTopMenu extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n',
|
text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(color: Colors.white),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
|
text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(color: Theme.of(context).hintColor),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return FlowyTooltip(
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
|
child: FlowyTooltip(
|
||||||
richMessage: textSpan,
|
richMessage: textSpan,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
width: PlatformExtension.isWindows ? 30 : 28,
|
width: 24,
|
||||||
hoverColor: Colors.transparent,
|
|
||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
.read<HomeSettingBloc>()
|
.read<HomeSettingBloc>()
|
||||||
.add(const HomeSettingEvent.collapseMenu()),
|
.add(const HomeSettingEvent.collapseMenu()),
|
||||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
iconPadding: const EdgeInsets.all(2),
|
||||||
icon: const FlowySvg(FlowySvgs.hide_menu_m),
|
icon: const FlowySvg(FlowySvgs.hide_menu_s),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,8 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||||
@ -10,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
// keep this widget in case we need to roll back (lucas.xu)
|
// keep this widget in case we need to roll back (lucas.xu)
|
||||||
@ -29,11 +28,14 @@ class SidebarUser extends StatelessWidget {
|
|||||||
child: BlocBuilder<MenuUserBloc, MenuUserState>(
|
child: BlocBuilder<MenuUserBloc, MenuUserState>(
|
||||||
builder: (context, state) => Row(
|
builder: (context, state) => Row(
|
||||||
children: [
|
children: [
|
||||||
|
const HSpace(6),
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
iconUrl: state.userProfile.iconUrl,
|
iconUrl: state.userProfile.iconUrl,
|
||||||
name: state.userProfile.name,
|
name: state.userProfile.name,
|
||||||
|
size: 16.0,
|
||||||
|
fontSize: 10.0,
|
||||||
),
|
),
|
||||||
const HSpace(8),
|
const HSpace(10),
|
||||||
Expanded(child: _buildUserName(context, state)),
|
Expanded(child: _buildUserName(context, state)),
|
||||||
UserSettingButton(userProfile: state.userProfile),
|
UserSettingButton(userProfile: state.userProfile),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
@ -7,11 +5,12 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SidebarFolder extends StatelessWidget {
|
class SidebarFolder extends StatelessWidget {
|
||||||
@ -38,7 +37,7 @@ class SidebarFolder extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
padding: const EdgeInsets.only(top: 16.0, bottom: 10),
|
||||||
child: FavoriteFolder(views: state.views),
|
child: FavoriteFolder(views: state.views),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -85,7 +84,7 @@ class PrivateSectionFolder extends SectionFolder {
|
|||||||
PrivateSectionFolder({super.key, required super.views})
|
PrivateSectionFolder({super.key, required super.views})
|
||||||
: super(
|
: super(
|
||||||
title: LocaleKeys.sideBar_private.tr(),
|
title: LocaleKeys.sideBar_private.tr(),
|
||||||
categoryType: FolderCategoryType.private,
|
spaceType: FolderSpaceType.private,
|
||||||
expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(),
|
expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(),
|
||||||
addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(),
|
addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(),
|
||||||
);
|
);
|
||||||
@ -95,7 +94,7 @@ class PublicSectionFolder extends SectionFolder {
|
|||||||
PublicSectionFolder({super.key, required super.views})
|
PublicSectionFolder({super.key, required super.views})
|
||||||
: super(
|
: super(
|
||||||
title: LocaleKeys.sideBar_workspace.tr(),
|
title: LocaleKeys.sideBar_workspace.tr(),
|
||||||
categoryType: FolderCategoryType.public,
|
spaceType: FolderSpaceType.public,
|
||||||
expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(),
|
expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(),
|
||||||
addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(),
|
addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(),
|
||||||
);
|
);
|
||||||
@ -105,7 +104,7 @@ class PersonalSectionFolder extends SectionFolder {
|
|||||||
PersonalSectionFolder({super.key, required super.views})
|
PersonalSectionFolder({super.key, required super.views})
|
||||||
: super(
|
: super(
|
||||||
title: LocaleKeys.sideBar_personal.tr(),
|
title: LocaleKeys.sideBar_personal.tr(),
|
||||||
categoryType: FolderCategoryType.public,
|
spaceType: FolderSpaceType.public,
|
||||||
expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(),
|
expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(),
|
||||||
addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(),
|
addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(),
|
||||||
);
|
);
|
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class SidebarNewPageButton extends StatelessWidget {
|
||||||
|
const SidebarNewPageButton({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
height: HomeSizes.newPageSectionHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
onTap: () async => _createNewPage(context),
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.new_app_s,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: SizedBox(
|
||||||
|
height: 18.0,
|
||||||
|
child: FlowyText.regular(
|
||||||
|
LocaleKeys.newPageText.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createNewPage(BuildContext context) async {
|
||||||
|
return createViewAndShowRenameDialogIfNeeded(
|
||||||
|
context,
|
||||||
|
LocaleKeys.newPageText.tr(),
|
||||||
|
(viewName, _) {
|
||||||
|
if (viewName.isNotEmpty) {
|
||||||
|
// if the workspace is collaborative, create the view in the private section by default.
|
||||||
|
final section =
|
||||||
|
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn
|
||||||
|
? ViewSectionPB.Private
|
||||||
|
: ViewSectionPB.Public;
|
||||||
|
context.read<SidebarSectionsBloc>().add(
|
||||||
|
SidebarSectionsEvent.createRootViewInSection(
|
||||||
|
name: viewName,
|
||||||
|
viewSection: section,
|
||||||
|
index: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -12,7 +11,9 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
|||||||
show UserProfilePB;
|
show UserProfilePB;
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
|
|
||||||
@ -47,15 +48,14 @@ class UserSettingButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyTooltip(
|
return SizedBox.square(
|
||||||
|
dimension: HomeSizes.workspaceSectionHeight,
|
||||||
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.settings_menu_open.tr(),
|
message: LocaleKeys.settings_menu_open.tr(),
|
||||||
child: IconButton(
|
child: FlowyButton(
|
||||||
onPressed: () => showSettingsDialog(context, userProfile),
|
onTap: () => showSettingsDialog(context, userProfile),
|
||||||
icon: SizedBox.square(
|
text: const FlowySvg(
|
||||||
dimension: 20,
|
FlowySvgs.settings_s,
|
||||||
child: FlowySvg(
|
|
||||||
FlowySvgs.settings_m,
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
@ -1,7 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
@ -17,12 +15,13 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||||
show UserProfilePB;
|
show UserProfilePB;
|
||||||
@ -31,6 +30,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
/// Home Sidebar is the left side bar of the home page.
|
/// Home Sidebar is the left side bar of the home page.
|
||||||
@ -222,7 +222,8 @@ class _SidebarState extends State<_Sidebar> {
|
|||||||
// top menu
|
// top menu
|
||||||
const Padding(padding: menuHorizontalInset, child: SidebarTopMenu()),
|
const Padding(padding: menuHorizontalInset, child: SidebarTopMenu()),
|
||||||
// user or workspace, setting
|
// user or workspace, setting
|
||||||
Padding(
|
Container(
|
||||||
|
height: HomeSizes.workspaceSectionHeight,
|
||||||
padding: menuHorizontalInset,
|
padding: menuHorizontalInset,
|
||||||
child:
|
child:
|
||||||
// if the workspaces are empty, show the user profile instead
|
// if the workspaces are empty, show the user profile instead
|
||||||
@ -231,12 +232,15 @@ class _SidebarState extends State<_Sidebar> {
|
|||||||
: SidebarUser(userProfile: widget.userProfile),
|
: SidebarUser(userProfile: widget.userProfile),
|
||||||
),
|
),
|
||||||
if (FeatureFlag.search.isOn) ...[
|
if (FeatureFlag.search.isOn) ...[
|
||||||
const VSpace(8),
|
const VSpace(6),
|
||||||
const Padding(
|
Container(
|
||||||
padding: menuHorizontalInset,
|
padding: menuHorizontalInset,
|
||||||
child: _SidebarSearchButton(),
|
height: HomeSizes.searchSectionHeight,
|
||||||
|
child: const _SidebarSearchButton(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
// new page button
|
||||||
|
const SidebarNewPageButton(),
|
||||||
// scrollable document list
|
// scrollable document list
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -256,11 +260,14 @@ class _SidebarState extends State<_Sidebar> {
|
|||||||
// trash
|
// trash
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: menuHorizontalInset,
|
padding: menuHorizontalInset,
|
||||||
child: SidebarTrashButton(),
|
child: Divider(height: 1.0, color: Color(0x141F2329)),
|
||||||
|
),
|
||||||
|
const VSpace(14),
|
||||||
|
const Padding(
|
||||||
|
padding: menuHorizontalInset,
|
||||||
|
child: SidebarFooter(),
|
||||||
),
|
),
|
||||||
const VSpace(10),
|
const VSpace(10),
|
||||||
// new page button
|
|
||||||
const SidebarNewPageButton(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -289,7 +296,8 @@ class _SidebarSearchButton extends StatelessWidget {
|
|||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
onTap: () => CommandPalette.of(context).toggle(),
|
onTap: () => CommandPalette.of(context).toggle(),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.search_s),
|
leftIcon: const FlowySvg(FlowySvgs.search_s),
|
||||||
text: FlowyText(LocaleKeys.search_label.tr()),
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText.regular(LocaleKeys.search_label.tr()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class SidebarNewPageButton extends StatelessWidget {
|
|
||||||
const SidebarNewPageButton({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final child = FlowyTextButton(
|
|
||||||
LocaleKeys.newPageText.tr(),
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
hoverColor: Colors.transparent,
|
|
||||||
fontColor: Theme.of(context).colorScheme.tertiary,
|
|
||||||
onPressed: () async => createViewAndShowRenameDialogIfNeeded(
|
|
||||||
context,
|
|
||||||
LocaleKeys.newPageText.tr(),
|
|
||||||
(viewName, _) {
|
|
||||||
if (viewName.isNotEmpty) {
|
|
||||||
// if the workspace is collaborative, create the view in the private section by default.
|
|
||||||
final section =
|
|
||||||
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn
|
|
||||||
? ViewSectionPB.Private
|
|
||||||
: ViewSectionPB.Public;
|
|
||||||
context.read<SidebarSectionsBloc>().add(
|
|
||||||
SidebarSectionsEvent.createRootViewInSection(
|
|
||||||
name: viewName,
|
|
||||||
viewSection: section,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
heading: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: FlowySvg(
|
|
||||||
FlowySvgs.new_app_s,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
);
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 60,
|
|
||||||
child: TopBorder(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:appflowy/startup/startup.dart';
|
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
|
|
||||||
class SidebarTrashButton extends StatelessWidget {
|
|
||||||
const SidebarTrashButton({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: getIt<MenuSharedState>().notifier,
|
|
||||||
builder: (context, value, child) {
|
|
||||||
return FlowyHover(
|
|
||||||
style: HoverStyle(
|
|
||||||
hoverColor: AFThemeExtension.of(context).greySelect,
|
|
||||||
),
|
|
||||||
isSelected: () => getIt<MenuSharedState>().latestOpenView == null,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 26,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
getIt<MenuSharedState>().latestOpenView = null;
|
|
||||||
getIt<TabsBloc>().add(
|
|
||||||
TabsEvent.openPlugin(
|
|
||||||
plugin: makePlugin(pluginType: PluginType.trash),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _buildTextButton(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTextButton(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const HSpace(6),
|
|
||||||
const FlowySvg(
|
|
||||||
FlowySvgs.trash_m,
|
|
||||||
size: Size(16, 16),
|
|
||||||
),
|
|
||||||
const HSpace(6),
|
|
||||||
FlowyText.medium(
|
|
||||||
LocaleKeys.trash_text.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,6 +16,7 @@ enum WorkspaceMoreAction {
|
|||||||
rename,
|
rename,
|
||||||
delete,
|
delete,
|
||||||
leave,
|
leave,
|
||||||
|
divider,
|
||||||
}
|
}
|
||||||
|
|
||||||
class WorkspaceMoreActionList extends StatelessWidget {
|
class WorkspaceMoreActionList extends StatelessWidget {
|
||||||
@ -32,6 +33,7 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
|||||||
final actions = [];
|
final actions = [];
|
||||||
if (myRole.isOwner) {
|
if (myRole.isOwner) {
|
||||||
actions.add(WorkspaceMoreAction.rename);
|
actions.add(WorkspaceMoreAction.rename);
|
||||||
|
actions.add(WorkspaceMoreAction.divider);
|
||||||
actions.add(WorkspaceMoreAction.delete);
|
actions.add(WorkspaceMoreAction.delete);
|
||||||
} else if (myRole.canLeave) {
|
} else if (myRole.canLeave) {
|
||||||
actions.add(WorkspaceMoreAction.leave);
|
actions.add(WorkspaceMoreAction.leave);
|
||||||
@ -40,20 +42,23 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return PopoverActionList<_WorkspaceMoreActionWrapper>(
|
return PopoverActionList<_WorkspaceMoreActionWrapper>(
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
actions: actions
|
actions: actions
|
||||||
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
|
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
constraints: const BoxConstraints(minWidth: 220),
|
||||||
buildChild: (controller) {
|
buildChild: (controller) {
|
||||||
return FlowyButton(
|
return SizedBox.square(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
dimension: 24.0,
|
||||||
useIntrinsicWidth: true,
|
child: FlowyButton(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
text: const FlowySvg(
|
text: const FlowySvg(
|
||||||
FlowySvgs.three_dots_vertical_s,
|
FlowySvgs.workspace_three_dots_s,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.show();
|
controller.show();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSelected: (action, controller) {},
|
onSelected: (action, controller) {},
|
||||||
@ -68,20 +73,37 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
|||||||
final UserWorkspacePB workspace;
|
final UserWorkspacePB workspace;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildWithContext(BuildContext context) {
|
Widget buildWithContext(BuildContext context, PopoverController controller) {
|
||||||
|
if (inner == WorkspaceMoreAction.divider) {
|
||||||
|
return const Divider();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildActionButton(context, controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButton(
|
||||||
|
BuildContext context,
|
||||||
|
PopoverController controller,
|
||||||
|
) {
|
||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
text: FlowyText(
|
leftIcon: buildLeftIcon(context),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText.regular(
|
||||||
name,
|
name,
|
||||||
color: inner == WorkspaceMoreAction.delete
|
fontSize: 14.0,
|
||||||
|
color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave]
|
||||||
|
.contains(inner)
|
||||||
? Theme.of(context).colorScheme.error
|
? Theme.of(context).colorScheme.error
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
margin: const EdgeInsets.all(6),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
PopoverContainer.of(context).closeAll();
|
PopoverContainer.of(context).closeAll();
|
||||||
|
|
||||||
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
||||||
switch (inner) {
|
switch (inner) {
|
||||||
|
case WorkspaceMoreAction.divider:
|
||||||
|
break;
|
||||||
case WorkspaceMoreAction.delete:
|
case WorkspaceMoreAction.delete:
|
||||||
await NavigatorAlertDialog(
|
await NavigatorAlertDialog(
|
||||||
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
|
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
|
||||||
@ -93,7 +115,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
|||||||
).show(context);
|
).show(context);
|
||||||
case WorkspaceMoreAction.rename:
|
case WorkspaceMoreAction.rename:
|
||||||
await NavigatorTextFieldDialog(
|
await NavigatorTextFieldDialog(
|
||||||
title: LocaleKeys.workspace_create.tr(),
|
title: LocaleKeys.workspace_renameWorkspace.tr(),
|
||||||
value: workspace.name,
|
value: workspace.name,
|
||||||
hintText: '',
|
hintText: '',
|
||||||
autoSelectAllText: true,
|
autoSelectAllText: true,
|
||||||
@ -132,6 +154,27 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
|||||||
return LocaleKeys.button_rename.tr();
|
return LocaleKeys.button_rename.tr();
|
||||||
case WorkspaceMoreAction.leave:
|
case WorkspaceMoreAction.leave:
|
||||||
return LocaleKeys.workspace_leaveCurrentWorkspace.tr();
|
return LocaleKeys.workspace_leaveCurrentWorkspace.tr();
|
||||||
|
case WorkspaceMoreAction.divider:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLeftIcon(BuildContext context) {
|
||||||
|
switch (inner) {
|
||||||
|
case WorkspaceMoreAction.delete:
|
||||||
|
return FlowySvg(
|
||||||
|
FlowySvgs.delete_s,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
);
|
||||||
|
case WorkspaceMoreAction.rename:
|
||||||
|
return const FlowySvg(FlowySvgs.view_item_rename_s);
|
||||||
|
case WorkspaceMoreAction.leave:
|
||||||
|
return FlowySvg(
|
||||||
|
FlowySvgs.logout_s,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
);
|
||||||
|
case WorkspaceMoreAction.divider:
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
@ -14,12 +13,14 @@ class WorkspaceIcon extends StatefulWidget {
|
|||||||
required this.workspace,
|
required this.workspace,
|
||||||
required this.enableEdit,
|
required this.enableEdit,
|
||||||
required this.iconSize,
|
required this.iconSize,
|
||||||
|
required this.fontSize,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final UserWorkspacePB workspace;
|
final UserWorkspacePB workspace;
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
final bool enableEdit;
|
final bool enableEdit;
|
||||||
|
final double fontSize;
|
||||||
final void Function(EmojiPickerResult) onSelected;
|
final void Function(EmojiPickerResult) onSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,10 +36,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
? Container(
|
? Container(
|
||||||
width: widget.iconSize,
|
width: widget.iconSize,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: EmojiText(
|
child: FlowyText.emoji(
|
||||||
emoji: widget.workspace.icon,
|
widget.workspace.icon,
|
||||||
fontSize: widget.iconSize,
|
fontSize: widget.iconSize,
|
||||||
lineHeight: 1,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
@ -47,13 +47,13 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
height: max(widget.iconSize, 26),
|
height: max(widget.iconSize, 26),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ColorGenerator(widget.workspace.name).toColor(),
|
color: ColorGenerator(widget.workspace.name).toColor(),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
widget.workspace.name.isEmpty
|
widget.workspace.name.isEmpty
|
||||||
? ''
|
? ''
|
||||||
: widget.workspace.name.substring(0, 1),
|
: widget.workspace.name.substring(0, 1),
|
||||||
fontSize: 16,
|
fontSize: widget.fontSize,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -63,7 +63,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
constraints: BoxConstraints.loose(const Size(364, 356)),
|
||||||
clickHandler: PopoverClickHandler.gestureDetector,
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
popupBuilder: (_) => FlowyIconPicker(
|
popupBuilder: (_) => FlowyIconPicker(
|
||||||
onSelected: (result) {
|
onSelected: (result) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||||
@ -13,6 +13,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@ -38,7 +39,7 @@ class WorkspacesMenu extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// user email
|
// user email
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -50,18 +51,16 @@ class WorkspacesMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(4.0),
|
const HSpace(4.0),
|
||||||
FlowyButton(
|
const _WorkspaceMoreButton(),
|
||||||
key: createWorkspaceButtonKey,
|
const HSpace(8.0),
|
||||||
useIntrinsicWidth: true,
|
|
||||||
text: const FlowySvg(FlowySvgs.add_m),
|
|
||||||
onTap: () {
|
|
||||||
_showCreateWorkspaceDialog(context);
|
|
||||||
PopoverContainer.of(context).closeAll();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Divider(height: 1.0),
|
||||||
|
),
|
||||||
|
// workspace list
|
||||||
for (final workspace in workspaces) ...[
|
for (final workspace in workspaces) ...[
|
||||||
WorkspaceMenuItem(
|
WorkspaceMenuItem(
|
||||||
key: ValueKey(workspace.workspaceId),
|
key: ValueKey(workspace.workspaceId),
|
||||||
@ -69,8 +68,11 @@ class WorkspacesMenu extends StatelessWidget {
|
|||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
isSelected: workspace.workspaceId == currentWorkspace.workspaceId,
|
isSelected: workspace.workspaceId == currentWorkspace.workspaceId,
|
||||||
),
|
),
|
||||||
const VSpace(4.0),
|
const VSpace(6.0),
|
||||||
],
|
],
|
||||||
|
// add new workspace
|
||||||
|
const _CreateWorkspaceButton(),
|
||||||
|
const VSpace(6.0),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -86,20 +88,9 @@ class WorkspacesMenu extends StatelessWidget {
|
|||||||
|
|
||||||
return LocaleKeys.defaultUsername.tr();
|
return LocaleKeys.defaultUsername.tr();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showCreateWorkspaceDialog(BuildContext context) async {
|
|
||||||
if (context.mounted) {
|
|
||||||
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
|
||||||
await CreateWorkspaceDialog(
|
|
||||||
onConfirm: (name) {
|
|
||||||
workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name));
|
|
||||||
},
|
|
||||||
).show(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class WorkspaceMenuItem extends StatelessWidget {
|
class WorkspaceMenuItem extends StatefulWidget {
|
||||||
const WorkspaceMenuItem({
|
const WorkspaceMenuItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.workspace,
|
required this.workspace,
|
||||||
@ -111,33 +102,51 @@ class WorkspaceMenuItem extends StatelessWidget {
|
|||||||
final UserWorkspacePB workspace;
|
final UserWorkspacePB workspace;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
|
||||||
|
final ValueNotifier<bool> isHovered = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
isHovered.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (_) =>
|
create: (_) => WorkspaceMemberBloc(
|
||||||
WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace)
|
userProfile: widget.userProfile,
|
||||||
..add(const WorkspaceMemberEvent.initial()),
|
workspace: widget.workspace,
|
||||||
|
)..add(const WorkspaceMemberEvent.initial()),
|
||||||
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
|
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// settings right icon inside the flowy button will
|
// settings right icon inside the flowy button will
|
||||||
// cause the popover dismiss intermediately when click the right icon.
|
// cause the popover dismiss intermediately when click the right icon.
|
||||||
// so using the stack to put the right icon on the flowy button.
|
// so using the stack to put the right icon on the flowy button.
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 52,
|
height: 40,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => isHovered.value = true,
|
||||||
|
onExit: (_) => isHovered.value = false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
_WorkspaceInfo(
|
_WorkspaceInfo(
|
||||||
isSelected: isSelected,
|
isSelected: widget.isSelected,
|
||||||
workspace: workspace,
|
workspace: widget.workspace,
|
||||||
),
|
),
|
||||||
Positioned(left: 8, child: _buildLeftIcon(context)),
|
Positioned(left: 4, child: _buildLeftIcon(context)),
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12.0,
|
right: 4.0,
|
||||||
child: Align(child: _buildRightIcon(context)),
|
child: Align(child: _buildRightIcon(context, isHovered)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -145,17 +154,26 @@ class WorkspaceMenuItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLeftIcon(BuildContext context) {
|
Widget _buildLeftIcon(BuildContext context) {
|
||||||
return SizedBox.square(
|
return Container(
|
||||||
dimension: 32,
|
width: 32.0,
|
||||||
|
height: 32.0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0x01717171).withOpacity(0.12),
|
||||||
|
width: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.document_plugins_cover_changeIcon.tr(),
|
message: LocaleKeys.document_plugins_cover_changeIcon.tr(),
|
||||||
child: WorkspaceIcon(
|
child: WorkspaceIcon(
|
||||||
workspace: workspace,
|
workspace: widget.workspace,
|
||||||
iconSize: 26,
|
iconSize: 22,
|
||||||
|
fontSize: 16,
|
||||||
enableEdit: true,
|
enableEdit: true,
|
||||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
workspace.workspaceId,
|
widget.workspace.workspaceId,
|
||||||
result.emoji,
|
result.emoji,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -164,19 +182,39 @@ class WorkspaceMenuItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRightIcon(BuildContext context) {
|
Widget _buildRightIcon(BuildContext context, ValueNotifier<bool> isHovered) {
|
||||||
// only the owner can update or delete workspace.
|
// only the owner can update or delete workspace.
|
||||||
// only show the more action button when the workspace is selected.
|
if (context.read<WorkspaceMemberBloc>().state.isLoading) {
|
||||||
if (!isSelected || context.read<WorkspaceMemberBloc>().state.isLoading) {
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
WorkspaceMoreActionList(workspace: workspace),
|
ValueListenableBuilder(
|
||||||
const FlowySvg(
|
valueListenable: isHovered,
|
||||||
FlowySvgs.blue_check_s,
|
builder: (context, value, child) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: value ? 1.0 : 0.0,
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: WorkspaceMoreActionList(workspace: widget.workspace),
|
||||||
|
),
|
||||||
|
const HSpace(8.0),
|
||||||
|
if (widget.isSelected) ...[
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(5.0),
|
||||||
|
child: FlowySvg(
|
||||||
|
FlowySvgs.workspace_selected_s,
|
||||||
|
blendMode: null,
|
||||||
|
size: Size.square(14.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(8.0),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -199,10 +237,9 @@ class _WorkspaceInfo extends StatelessWidget {
|
|||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
onTap: () => _openWorkspace(context),
|
onTap: () => _openWorkspace(context),
|
||||||
iconPadding: 10.0,
|
iconPadding: 10.0,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
|
||||||
leftIconSize: const Size.square(32),
|
leftIconSize: const Size.square(32),
|
||||||
leftIcon: const SizedBox.square(dimension: 32),
|
leftIcon: const SizedBox.square(dimension: 32),
|
||||||
rightIcon: const HSpace(42.0),
|
rightIcon: const HSpace(32.0),
|
||||||
text: Column(
|
text: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -213,8 +250,9 @@ class _WorkspaceInfo extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
withTooltip: true,
|
withTooltip: true,
|
||||||
),
|
),
|
||||||
|
const VSpace(2.0),
|
||||||
// workspace members count
|
// workspace members count
|
||||||
FlowyText(
|
FlowyText.regular(
|
||||||
state.isLoading
|
state.isLoading
|
||||||
? ''
|
? ''
|
||||||
: LocaleKeys.settings_appearance_members_membersCount
|
: LocaleKeys.settings_appearance_members_membersCount
|
||||||
@ -263,3 +301,90 @@ class CreateWorkspaceDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CreateWorkspaceButton extends StatelessWidget {
|
||||||
|
const _CreateWorkspaceButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: FlowyButton(
|
||||||
|
key: createWorkspaceButtonKey,
|
||||||
|
onTap: () {
|
||||||
|
_showCreateWorkspaceDialog(context);
|
||||||
|
PopoverContainer.of(context).closeAll();
|
||||||
|
},
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
text: Row(
|
||||||
|
children: [
|
||||||
|
_buildLeftIcon(context),
|
||||||
|
const HSpace(10.0),
|
||||||
|
FlowyText.regular(LocaleKeys.workspace_create.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLeftIcon(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 32.0,
|
||||||
|
height: 32.0,
|
||||||
|
padding: const EdgeInsets.all(7.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0x01717171).withOpacity(0.12),
|
||||||
|
width: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const FlowySvg(FlowySvgs.add_workspace_s),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showCreateWorkspaceDialog(BuildContext context) async {
|
||||||
|
if (context.mounted) {
|
||||||
|
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
||||||
|
await CreateWorkspaceDialog(
|
||||||
|
onConfirm: (name) {
|
||||||
|
workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name));
|
||||||
|
},
|
||||||
|
).show(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WorkspaceMoreButton extends StatelessWidget {
|
||||||
|
const _WorkspaceMoreButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
popupBuilder: (_) => FlowyButton(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
|
||||||
|
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText.regular(LocaleKeys.button_logout.tr()),
|
||||||
|
onTap: () async {
|
||||||
|
await getIt<AuthService>().signOut();
|
||||||
|
await runAppFlowy();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 24.0,
|
||||||
|
child: FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
text: const FlowySvg(
|
||||||
|
FlowySvgs.workspace_three_dots_s,
|
||||||
|
size: Size.square(16.0),
|
||||||
|
),
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
@ -16,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
|||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SidebarWorkspace extends StatefulWidget {
|
class SidebarWorkspace extends StatefulWidget {
|
||||||
@ -50,8 +49,9 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
UserSettingButton(userProfile: widget.userProfile),
|
UserSettingButton(userProfile: widget.userProfile),
|
||||||
const HSpace(4),
|
const HSpace(8),
|
||||||
const NotificationButton(),
|
const NotificationButton(),
|
||||||
|
const HSpace(4),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -144,7 +144,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
class SidebarSwitchWorkspaceButton extends StatefulWidget {
|
||||||
const SidebarSwitchWorkspaceButton({
|
const SidebarSwitchWorkspaceButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.userProfile,
|
required this.userProfile,
|
||||||
@ -154,16 +154,31 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
|||||||
final UserWorkspacePB currentWorkspace;
|
final UserWorkspacePB currentWorkspace;
|
||||||
final UserProfilePB userProfile;
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SidebarSwitchWorkspaceButton> createState() =>
|
||||||
|
_SidebarSwitchWorkspaceButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidebarSwitchWorkspaceButtonState
|
||||||
|
extends State<SidebarSwitchWorkspaceButton> {
|
||||||
|
final ValueNotifier<bool> _isWorkSpaceMenuExpanded = ValueNotifier(false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 5),
|
||||||
constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600),
|
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||||
onOpen: () => context
|
onOpen: () {
|
||||||
|
_isWorkSpaceMenuExpanded.value = true;
|
||||||
|
context
|
||||||
.read<UserWorkspaceBloc>()
|
.read<UserWorkspaceBloc>()
|
||||||
.add(const UserWorkspaceEvent.fetchWorkspaces()),
|
.add(const UserWorkspaceEvent.fetchWorkspaces());
|
||||||
onClose: () => Log.info('close workspace menu'),
|
},
|
||||||
|
onClose: () {
|
||||||
|
_isWorkSpaceMenuExpanded.value = false;
|
||||||
|
Log.info('close workspace menu');
|
||||||
|
},
|
||||||
popupBuilder: (_) {
|
popupBuilder: (_) {
|
||||||
return BlocProvider<UserWorkspaceBloc>.value(
|
return BlocProvider<UserWorkspaceBloc>.value(
|
||||||
value: context.read<UserWorkspaceBloc>(),
|
value: context.read<UserWorkspaceBloc>(),
|
||||||
@ -176,7 +191,7 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
Log.info('open workspace menu');
|
Log.info('open workspace menu');
|
||||||
return WorkspacesMenu(
|
return WorkspacesMenu(
|
||||||
userProfile: userProfile,
|
userProfile: widget.userProfile,
|
||||||
currentWorkspace: currentWorkspace,
|
currentWorkspace: currentWorkspace,
|
||||||
workspaces: workspaces,
|
workspaces: workspaces,
|
||||||
);
|
);
|
||||||
@ -185,33 +200,42 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
margin: EdgeInsets.zero,
|
||||||
text: Row(
|
text: Row(
|
||||||
children: [
|
children: [
|
||||||
const HSpace(2.0),
|
const HSpace(6.0),
|
||||||
SizedBox.square(
|
SizedBox(
|
||||||
dimension: 30.0,
|
width: 16.0,
|
||||||
child: WorkspaceIcon(
|
child: WorkspaceIcon(
|
||||||
workspace: currentWorkspace,
|
workspace: widget.currentWorkspace,
|
||||||
iconSize: 20,
|
iconSize: 16,
|
||||||
|
fontSize: 10,
|
||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
currentWorkspace.workspaceId,
|
widget.currentWorkspace.workspaceId,
|
||||||
result.emoji,
|
result.emoji,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(6),
|
const HSpace(10),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
currentWorkspace.name,
|
widget.currentWorkspace.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
withTooltip: true,
|
withTooltip: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const FlowySvg(FlowySvgs.drop_menu_show_m),
|
const HSpace(4),
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: _isWorkSpaceMenuExpanded,
|
||||||
|
builder: (context, value, _) => FlowySvg(
|
||||||
|
value
|
||||||
|
? FlowySvgs.workspace_drop_down_menu_hide_s
|
||||||
|
: FlowySvgs.workspace_drop_down_menu_show_s,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
@ -15,6 +15,8 @@ enum DraggableHoverPosition {
|
|||||||
bottom,
|
bottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kDraggableViewItemDividerHeight = 2.0;
|
||||||
|
|
||||||
class DraggableViewItem extends StatefulWidget {
|
class DraggableViewItem extends StatefulWidget {
|
||||||
const DraggableViewItem({
|
const DraggableViewItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -45,8 +47,7 @@ class DraggableViewItem extends StatefulWidget {
|
|||||||
|
|
||||||
class _DraggableViewItemState extends State<DraggableViewItem> {
|
class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||||
DraggableHoverPosition position = DraggableHoverPosition.none;
|
DraggableHoverPosition position = DraggableHoverPosition.none;
|
||||||
|
final hoverColor = const Color(0xFF00C8FF);
|
||||||
final _dividerHeight = 2.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -100,29 +101,26 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
|||||||
// only show the top border when the draggable item is the first child
|
// only show the top border when the draggable item is the first child
|
||||||
if (widget.isFirstChild)
|
if (widget.isFirstChild)
|
||||||
Divider(
|
Divider(
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
thickness: _dividerHeight,
|
thickness: kDraggableViewItemDividerHeight,
|
||||||
color: position == DraggableHoverPosition.top
|
color: position == DraggableHoverPosition.top
|
||||||
? widget.topHighlightColor ??
|
? widget.topHighlightColor ?? hoverColor
|
||||||
Theme.of(context).colorScheme.secondary
|
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
DecoratedBox(
|
DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
color: position == DraggableHoverPosition.center
|
color: position == DraggableHoverPosition.center
|
||||||
? widget.centerHighlightColor ??
|
? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5)
|
||||||
Theme.of(context).colorScheme.secondary.withOpacity(0.5)
|
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
Divider(
|
Divider(
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
thickness: _dividerHeight,
|
thickness: kDraggableViewItemDividerHeight,
|
||||||
color: position == DraggableHoverPosition.bottom
|
color: position == DraggableHoverPosition.bottom
|
||||||
? widget.bottomHighlightColor ??
|
? widget.bottomHighlightColor ?? hoverColor
|
||||||
Theme.of(context).colorScheme.secondary
|
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -137,10 +135,10 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
child: Divider(
|
child: Divider(
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
thickness: _dividerHeight,
|
thickness: kDraggableViewItemDividerHeight,
|
||||||
color: position == DraggableHoverPosition.top
|
color: position == DraggableHoverPosition.top
|
||||||
? widget.topHighlightColor ??
|
? widget.topHighlightColor ??
|
||||||
Theme.of(context).colorScheme.secondary
|
Theme.of(context).colorScheme.secondary
|
||||||
@ -161,10 +159,10 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
child: Divider(
|
child: Divider(
|
||||||
height: _dividerHeight,
|
height: kDraggableViewItemDividerHeight,
|
||||||
thickness: _dividerHeight,
|
thickness: kDraggableViewItemDividerHeight,
|
||||||
color: position == DraggableHoverPosition.bottom
|
color: position == DraggableHoverPosition.bottom
|
||||||
? widget.bottomHighlightColor ??
|
? widget.bottomHighlightColor ??
|
||||||
Theme.of(context).colorScheme.secondary
|
Theme.of(context).colorScheme.secondary
|
||||||
|
@ -10,8 +10,13 @@ enum ViewMoreActionType {
|
|||||||
duplicate,
|
duplicate,
|
||||||
copyLink, // not supported yet.
|
copyLink, // not supported yet.
|
||||||
rename,
|
rename,
|
||||||
moveTo, // not supported yet.
|
moveTo,
|
||||||
openInNewTab,
|
openInNewTab,
|
||||||
|
changeIcon,
|
||||||
|
collapseAllPages, // including sub pages
|
||||||
|
divider,
|
||||||
|
lastModified,
|
||||||
|
created,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||||
@ -33,27 +38,63 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
|||||||
return LocaleKeys.disclosureAction_moveTo.tr();
|
return LocaleKeys.disclosureAction_moveTo.tr();
|
||||||
case ViewMoreActionType.openInNewTab:
|
case ViewMoreActionType.openInNewTab:
|
||||||
return LocaleKeys.disclosureAction_openNewTab.tr();
|
return LocaleKeys.disclosureAction_openNewTab.tr();
|
||||||
|
case ViewMoreActionType.changeIcon:
|
||||||
|
return LocaleKeys.disclosureAction_changeIcon.tr();
|
||||||
|
case ViewMoreActionType.collapseAllPages:
|
||||||
|
return LocaleKeys.disclosureAction_collapseAllPages.tr();
|
||||||
|
case ViewMoreActionType.divider:
|
||||||
|
case ViewMoreActionType.lastModified:
|
||||||
|
case ViewMoreActionType.created:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget icon(Color iconColor) {
|
Widget get leftIcon {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ViewMoreActionType.delete:
|
case ViewMoreActionType.delete:
|
||||||
return const FlowySvg(FlowySvgs.delete_s);
|
return const FlowySvg(FlowySvgs.trash_s, blendMode: null);
|
||||||
case ViewMoreActionType.favorite:
|
case ViewMoreActionType.favorite:
|
||||||
return const FlowySvg(FlowySvgs.unfavorite_s);
|
|
||||||
case ViewMoreActionType.unFavorite:
|
|
||||||
return const FlowySvg(FlowySvgs.favorite_s);
|
return const FlowySvg(FlowySvgs.favorite_s);
|
||||||
|
case ViewMoreActionType.unFavorite:
|
||||||
|
return const FlowySvg(FlowySvgs.unfavorite_s);
|
||||||
case ViewMoreActionType.duplicate:
|
case ViewMoreActionType.duplicate:
|
||||||
return const FlowySvg(FlowySvgs.copy_s);
|
return const FlowySvg(FlowySvgs.duplicate_s);
|
||||||
case ViewMoreActionType.copyLink:
|
case ViewMoreActionType.copyLink:
|
||||||
return const Icon(Icons.copy);
|
return const Icon(Icons.copy);
|
||||||
case ViewMoreActionType.rename:
|
case ViewMoreActionType.rename:
|
||||||
return const FlowySvg(FlowySvgs.edit_s);
|
return const FlowySvg(FlowySvgs.view_item_rename_s);
|
||||||
case ViewMoreActionType.moveTo:
|
case ViewMoreActionType.moveTo:
|
||||||
return const Icon(Icons.move_to_inbox);
|
return const FlowySvg(FlowySvgs.move_to_s);
|
||||||
case ViewMoreActionType.openInNewTab:
|
case ViewMoreActionType.openInNewTab:
|
||||||
return const FlowySvg(FlowySvgs.full_view_s);
|
return const FlowySvg(FlowySvgs.view_item_open_in_new_tab_s);
|
||||||
|
case ViewMoreActionType.changeIcon:
|
||||||
|
return const FlowySvg(FlowySvgs.change_icon_s);
|
||||||
|
case ViewMoreActionType.collapseAllPages:
|
||||||
|
return const FlowySvg(FlowySvgs.collapse_all_page_s);
|
||||||
|
case ViewMoreActionType.divider:
|
||||||
|
case ViewMoreActionType.lastModified:
|
||||||
|
case ViewMoreActionType.created:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get rightIcon {
|
||||||
|
switch (this) {
|
||||||
|
case ViewMoreActionType.changeIcon:
|
||||||
|
case ViewMoreActionType.moveTo:
|
||||||
|
return const FlowySvg(FlowySvgs.view_item_right_arrow_s);
|
||||||
|
case ViewMoreActionType.favorite:
|
||||||
|
case ViewMoreActionType.unFavorite:
|
||||||
|
case ViewMoreActionType.duplicate:
|
||||||
|
case ViewMoreActionType.copyLink:
|
||||||
|
case ViewMoreActionType.rename:
|
||||||
|
case ViewMoreActionType.openInNewTab:
|
||||||
|
case ViewMoreActionType.collapseAllPages:
|
||||||
|
case ViewMoreActionType.divider:
|
||||||
|
case ViewMoreActionType.delete:
|
||||||
|
case ViewMoreActionType.lastModified:
|
||||||
|
case ViewMoreActionType.created:
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/document.dart';
|
import 'package:appflowy/plugins/document/document.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart';
|
||||||
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
class ViewAddButton extends StatelessWidget {
|
class ViewAddButton extends StatelessWidget {
|
||||||
const ViewAddButton({
|
const ViewAddButton({
|
||||||
@ -51,13 +50,12 @@ class ViewAddButton extends StatelessWidget {
|
|||||||
return PopoverActionList<PopoverAction>(
|
return PopoverActionList<PopoverAction>(
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
actions: _actions,
|
actions: _actions,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 4),
|
||||||
buildChild: (popover) {
|
buildChild: (popover) {
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
iconPadding: const EdgeInsets.all(2),
|
width: 24,
|
||||||
width: 26,
|
icon: const FlowySvg(FlowySvgs.view_item_add_s),
|
||||||
icon: const FlowySvg(FlowySvgs.add_s),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onEditing(true);
|
onEditing(true);
|
||||||
popover.show();
|
popover.show();
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
@ -11,8 +8,9 @@ import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_b
|
|||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.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';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||||
@ -26,16 +24,25 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
typedef ViewItemOnSelected = void Function(ViewPB, BuildContext);
|
typedef ViewItemOnSelected = void Function(BuildContext context, ViewPB view);
|
||||||
|
typedef ViewItemLeftIconBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
ViewPB view,
|
||||||
|
);
|
||||||
|
typedef ViewItemRightIconsBuilder = List<Widget> Function(
|
||||||
|
BuildContext context,
|
||||||
|
ViewPB view,
|
||||||
|
);
|
||||||
|
|
||||||
class ViewItem extends StatelessWidget {
|
class ViewItem extends StatelessWidget {
|
||||||
const ViewItem({
|
const ViewItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
this.parentView,
|
this.parentView,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
required this.level,
|
required this.level,
|
||||||
this.leftPadding = 10,
|
this.leftPadding = 10,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
@ -43,15 +50,19 @@ class ViewItem extends StatelessWidget {
|
|||||||
this.isFirstChild = false,
|
this.isFirstChild = false,
|
||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
required this.isFeedback,
|
required this.isFeedback,
|
||||||
this.height = 28.0,
|
this.height = HomeSpaceViewSizes.viewHeight,
|
||||||
this.isHoverEnabled = true,
|
this.isHoverEnabled = true,
|
||||||
this.isPlaceholder = false,
|
this.isPlaceholder = false,
|
||||||
|
this.isHovered,
|
||||||
|
this.shouldRenderChildren = true,
|
||||||
|
this.leftIconBuilder,
|
||||||
|
this.rightIconsBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final ViewPB? parentView;
|
final ViewPB? parentView;
|
||||||
|
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
// indicate the level of the view item
|
// indicate the level of the view item
|
||||||
// used to calculate the left padding
|
// used to calculate the left padding
|
||||||
@ -85,6 +96,17 @@ class ViewItem extends StatelessWidget {
|
|||||||
// placeholder widget to receive the drop event when moving view across sections.
|
// placeholder widget to receive the drop event when moving view across sections.
|
||||||
final bool isPlaceholder;
|
final bool isPlaceholder;
|
||||||
|
|
||||||
|
// used for control the expand/collapse icon
|
||||||
|
final ValueNotifier<bool>? isHovered;
|
||||||
|
|
||||||
|
// render the child views of the view
|
||||||
|
final bool shouldRenderChildren;
|
||||||
|
|
||||||
|
// custom the left icon widget, if it's null, the default expand/collapse icon will be used
|
||||||
|
final ViewItemLeftIconBuilder? leftIconBuilder;
|
||||||
|
// custom the right icon widget, if it's null, the default ... and + button will be used
|
||||||
|
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
@ -100,7 +122,7 @@ class ViewItem extends StatelessWidget {
|
|||||||
view: state.view,
|
view: state.view,
|
||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
childViews: state.view.childViews,
|
childViews: state.view.childViews,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
level: level,
|
level: level,
|
||||||
leftPadding: leftPadding,
|
leftPadding: leftPadding,
|
||||||
showActions: state.isEditing,
|
showActions: state.isEditing,
|
||||||
@ -113,6 +135,10 @@ class ViewItem extends StatelessWidget {
|
|||||||
height: height,
|
height: height,
|
||||||
isHoverEnabled: isHoverEnabled,
|
isHoverEnabled: isHoverEnabled,
|
||||||
isPlaceholder: isPlaceholder,
|
isPlaceholder: isPlaceholder,
|
||||||
|
isHovered: isHovered,
|
||||||
|
shouldRenderChildren: shouldRenderChildren,
|
||||||
|
leftIconBuilder: leftIconBuilder,
|
||||||
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -128,7 +154,7 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
required this.view,
|
required this.view,
|
||||||
required this.parentView,
|
required this.parentView,
|
||||||
required this.childViews,
|
required this.childViews,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
this.isExpanded = true,
|
this.isExpanded = true,
|
||||||
required this.level,
|
required this.level,
|
||||||
@ -141,12 +167,16 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
required this.height,
|
required this.height,
|
||||||
this.isHoverEnabled = true,
|
this.isHoverEnabled = true,
|
||||||
this.isPlaceholder = false,
|
this.isPlaceholder = false,
|
||||||
|
this.isHovered,
|
||||||
|
this.shouldRenderChildren = true,
|
||||||
|
required this.leftIconBuilder,
|
||||||
|
required this.rightIconsBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final ViewPB? parentView;
|
final ViewPB? parentView;
|
||||||
final List<ViewPB> childViews;
|
final List<ViewPB> childViews;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
final bool isDraggable;
|
final bool isDraggable;
|
||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
@ -164,6 +194,10 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
|
|
||||||
final bool isHoverEnabled;
|
final bool isHoverEnabled;
|
||||||
final bool isPlaceholder;
|
final bool isPlaceholder;
|
||||||
|
final ValueNotifier<bool>? isHovered;
|
||||||
|
final bool shouldRenderChildren;
|
||||||
|
final ViewItemLeftIconBuilder? leftIconBuilder;
|
||||||
|
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -172,7 +206,7 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
level: level,
|
level: level,
|
||||||
showActions: showActions,
|
showActions: showActions,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
onTertiarySelected: onTertiarySelected,
|
onTertiarySelected: onTertiarySelected,
|
||||||
isExpanded: isExpanded,
|
isExpanded: isExpanded,
|
||||||
@ -181,16 +215,18 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
isFeedback: isFeedback,
|
isFeedback: isFeedback,
|
||||||
height: height,
|
height: height,
|
||||||
isPlaceholder: isPlaceholder,
|
isPlaceholder: isPlaceholder,
|
||||||
|
isHovered: isHovered,
|
||||||
|
leftIconBuilder: leftIconBuilder,
|
||||||
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
);
|
);
|
||||||
|
|
||||||
// if the view is expanded and has child views, render its child views
|
// if the view is expanded and has child views, render its child views
|
||||||
if (isExpanded) {
|
if (isExpanded && shouldRenderChildren && childViews.isNotEmpty) {
|
||||||
if (childViews.isNotEmpty) {
|
|
||||||
final children = childViews.map((childView) {
|
final children = childViews.map((childView) {
|
||||||
return ViewItem(
|
return ViewItem(
|
||||||
key: ValueKey('${categoryType.name} ${childView.id}'),
|
key: ValueKey('${spaceType.name} ${childView.id}'),
|
||||||
parentView: view,
|
parentView: view,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
isFirstChild: childView.id == childViews.first.id,
|
isFirstChild: childView.id == childViews.first.id,
|
||||||
view: childView,
|
view: childView,
|
||||||
level: level + 1,
|
level: level + 1,
|
||||||
@ -200,6 +236,9 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
leftPadding: leftPadding,
|
leftPadding: leftPadding,
|
||||||
isFeedback: isFeedback,
|
isFeedback: isFeedback,
|
||||||
isPlaceholder: isPlaceholder,
|
isPlaceholder: isPlaceholder,
|
||||||
|
isHovered: isHovered,
|
||||||
|
leftIconBuilder: leftIconBuilder,
|
||||||
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@ -210,27 +249,6 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
...children,
|
...children,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
child = Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
child,
|
|
||||||
Container(
|
|
||||||
height: height,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
// add 2px to make the text align with the view item
|
|
||||||
padding: EdgeInsets.only(left: (level + 1) * leftPadding + 2),
|
|
||||||
child: FlowyText.medium(
|
|
||||||
LocaleKeys.noPagesInside.tr(),
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrap the child with DraggableItem if isDraggable is true
|
// wrap the child with DraggableItem if isDraggable is true
|
||||||
@ -246,16 +264,27 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
? (from, to) => _moveViewCrossSection(context, from, to)
|
? (from, to) => _moveViewCrossSection(context, from, to)
|
||||||
: null,
|
: null,
|
||||||
feedback: (context) {
|
feedback: (context) {
|
||||||
return ViewItem(
|
return Container(
|
||||||
|
width: 250,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Brightness.light == Theme.of(context).brightness
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ViewItem(
|
||||||
view: view,
|
view: view,
|
||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
categoryType: categoryType,
|
spaceType: spaceType,
|
||||||
level: level,
|
level: level,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
onTertiarySelected: onTertiarySelected,
|
onTertiarySelected: onTertiarySelected,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
leftPadding: leftPadding,
|
leftPadding: leftPadding,
|
||||||
isFeedback: true,
|
isFeedback: true,
|
||||||
|
leftIconBuilder: leftIconBuilder,
|
||||||
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
@ -263,7 +292,7 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
// keep the same height of the DraggableItem
|
// keep the same height of the DraggableItem
|
||||||
child = Padding(
|
child = Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: kDraggableViewItemDividerHeight),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -279,10 +308,10 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
if (isReferencedDatabaseView(view, parentView)) {
|
if (isReferencedDatabaseView(view, parentView)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final fromSection = categoryType == FolderCategoryType.public
|
final fromSection = spaceType == FolderSpaceType.public
|
||||||
? ViewSectionPB.Private
|
? ViewSectionPB.Private
|
||||||
: ViewSectionPB.Public;
|
: ViewSectionPB.Public;
|
||||||
final toSection = categoryType == FolderCategoryType.public
|
final toSection = spaceType == FolderSpaceType.public
|
||||||
? ViewSectionPB.Public
|
? ViewSectionPB.Public
|
||||||
: ViewSectionPB.Private;
|
: ViewSectionPB.Private;
|
||||||
context.read<ViewBloc>().add(
|
context.read<ViewBloc>().add(
|
||||||
@ -297,7 +326,7 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
context.read<ViewBloc>().add(
|
context.read<ViewBloc>().add(
|
||||||
ViewEvent.updateViewVisibility(
|
ViewEvent.updateViewVisibility(
|
||||||
from,
|
from,
|
||||||
categoryType == FolderCategoryType.public,
|
spaceType == FolderSpaceType.public,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -312,7 +341,7 @@ class SingleInnerViewItem extends StatefulWidget {
|
|||||||
required this.level,
|
required this.level,
|
||||||
required this.leftPadding,
|
required this.leftPadding,
|
||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
required this.categoryType,
|
required this.spaceType,
|
||||||
required this.showActions,
|
required this.showActions,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
this.onTertiarySelected,
|
this.onTertiarySelected,
|
||||||
@ -320,6 +349,9 @@ class SingleInnerViewItem extends StatefulWidget {
|
|||||||
required this.height,
|
required this.height,
|
||||||
this.isHoverEnabled = true,
|
this.isHoverEnabled = true,
|
||||||
this.isPlaceholder = false,
|
this.isPlaceholder = false,
|
||||||
|
this.isHovered,
|
||||||
|
required this.leftIconBuilder,
|
||||||
|
required this.rightIconsBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -335,11 +367,14 @@ class SingleInnerViewItem extends StatefulWidget {
|
|||||||
final bool showActions;
|
final bool showActions;
|
||||||
final ViewItemOnSelected onSelected;
|
final ViewItemOnSelected onSelected;
|
||||||
final ViewItemOnSelected? onTertiarySelected;
|
final ViewItemOnSelected? onTertiarySelected;
|
||||||
final FolderCategoryType categoryType;
|
final FolderSpaceType spaceType;
|
||||||
final double height;
|
final double height;
|
||||||
|
|
||||||
final bool isHoverEnabled;
|
final bool isHoverEnabled;
|
||||||
final bool isPlaceholder;
|
final bool isPlaceholder;
|
||||||
|
final ValueNotifier<bool>? isHovered;
|
||||||
|
final ViewItemLeftIconBuilder? leftIconBuilder;
|
||||||
|
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
|
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
|
||||||
@ -382,11 +417,11 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
Widget _buildViewItem(bool onHover, [bool isSelected = false]) {
|
Widget _buildViewItem(bool onHover, [bool isSelected = false]) {
|
||||||
final children = [
|
final children = [
|
||||||
// expand icon
|
// expand icon or placeholder
|
||||||
_buildLeftIcon(),
|
widget.leftIconBuilder?.call(context, widget.view) ?? _buildLeftIcon(),
|
||||||
// icon
|
// icon
|
||||||
_buildViewIconButton(),
|
_buildViewIconButton(),
|
||||||
const HSpace(5),
|
const HSpace(6),
|
||||||
// title
|
// title
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText.regular(
|
child: FlowyText.regular(
|
||||||
@ -398,25 +433,32 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
// hover action
|
// hover action
|
||||||
if (widget.showActions || onHover) {
|
if (widget.showActions || onHover) {
|
||||||
|
if (widget.rightIconsBuilder != null) {
|
||||||
|
children.addAll(widget.rightIconsBuilder!(context, widget.view));
|
||||||
|
} else {
|
||||||
// ··· more action button
|
// ··· more action button
|
||||||
children.add(_buildViewMoreActionButton(context));
|
children.add(_buildViewMoreActionButton(context));
|
||||||
|
children.add(const HSpace(8.0));
|
||||||
// only support add button for document layout
|
// only support add button for document layout
|
||||||
if (widget.view.layout == ViewLayoutPB.Document) {
|
if (widget.view.layout == ViewLayoutPB.Document) {
|
||||||
// + button
|
// + button
|
||||||
children.add(_buildViewAddButton(context));
|
children.add(_buildViewAddButton(context));
|
||||||
}
|
}
|
||||||
|
children.add(const HSpace(4.0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final child = GestureDetector(
|
final child = GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () => widget.onSelected(widget.view, context),
|
onTap: () => widget.onSelected(context, widget.view),
|
||||||
onTertiaryTapDown: (_) =>
|
onTertiaryTapDown: (_) =>
|
||||||
widget.onTertiarySelected?.call(widget.view, context),
|
widget.onTertiarySelected?.call(context, widget.view),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
|
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -446,26 +488,24 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
Widget _buildViewIconButton() {
|
Widget _buildViewIconButton() {
|
||||||
final icon = widget.view.icon.value.isNotEmpty
|
final icon = widget.view.icon.value.isNotEmpty
|
||||||
? EmojiText(
|
? FlowyText.emoji(
|
||||||
emoji: widget.view.icon.value,
|
widget.view.icon.value,
|
||||||
fontSize: 18.0,
|
fontSize: 16.0,
|
||||||
)
|
)
|
||||||
: SizedBox.square(
|
: widget.view.defaultIcon();
|
||||||
dimension: 20.0,
|
|
||||||
child: widget.view.defaultIcon(),
|
|
||||||
);
|
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
offset: const Offset(20, 0),
|
offset: const Offset(20, 0),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
direction: PopoverDirection.rightWithCenterAligned,
|
direction: PopoverDirection.rightWithCenterAligned,
|
||||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
constraints: BoxConstraints.loose(const Size(364, 356)),
|
||||||
onClose: () => setState(() => isIconPickerOpened = false),
|
onClose: () => setState(() => isIconPickerOpened = false),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// prevent the tap event from being passed to the parent widget
|
// prevent the tap event from being passed to the parent widget
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: LocaleKeys.document_plugins_cover_changeIcon.tr(),
|
message: LocaleKeys.document_plugins_cover_changeIcon.tr(),
|
||||||
child: icon,
|
child: SizedBox(width: 16.0, child: icon),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
popupBuilder: (context) {
|
popupBuilder: (context) {
|
||||||
@ -492,18 +532,33 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
return const _DotIconWidget();
|
return const _DotIconWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
final svg = widget.isExpanded
|
if (context.read<ViewBloc>().state.view.childViews.isEmpty) {
|
||||||
? FlowySvgs.drop_menu_show_m
|
return HSpace(widget.leftPadding);
|
||||||
: FlowySvgs.drop_menu_hide_m;
|
}
|
||||||
return GestureDetector(
|
|
||||||
|
final child = GestureDetector(
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
svg,
|
widget.isExpanded
|
||||||
|
? FlowySvgs.view_item_expand_s
|
||||||
|
: FlowySvgs.view_item_unexpand_s,
|
||||||
size: const Size.square(16.0),
|
size: const Size.square(16.0),
|
||||||
),
|
),
|
||||||
onTap: () => context
|
onTap: () => context
|
||||||
.read<ViewBloc>()
|
.read<ViewBloc>()
|
||||||
.add(ViewEvent.setIsExpanded(!widget.isExpanded)),
|
.add(ViewEvent.setIsExpanded(!widget.isExpanded)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (widget.isHovered != null) {
|
||||||
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: widget.isHovered!,
|
||||||
|
builder: (_, isHovered, child) {
|
||||||
|
return Opacity(opacity: isHovered ? 1.0 : 0.0, child: child);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
// + button
|
// + button
|
||||||
@ -533,7 +588,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
viewName,
|
viewName,
|
||||||
pluginBuilder.layoutType!,
|
pluginBuilder.layoutType!,
|
||||||
openAfterCreated: openAfterCreated,
|
openAfterCreated: openAfterCreated,
|
||||||
section: widget.categoryType.toViewSectionPB,
|
section: widget.spaceType.toViewSectionPB,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -554,9 +609,10 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
||||||
child: ViewMoreActionButton(
|
child: ViewMoreActionButton(
|
||||||
view: widget.view,
|
view: widget.view,
|
||||||
|
spaceType: widget.spaceType,
|
||||||
onEditing: (value) =>
|
onEditing: (value) =>
|
||||||
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
|
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
|
||||||
onAction: (action) {
|
onAction: (action, data) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ViewMoreActionType.favorite:
|
case ViewMoreActionType.favorite:
|
||||||
case ViewMoreActionType.unFavorite:
|
case ViewMoreActionType.unFavorite:
|
||||||
@ -584,6 +640,20 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
case ViewMoreActionType.openInNewTab:
|
case ViewMoreActionType.openInNewTab:
|
||||||
context.read<TabsBloc>().openTab(widget.view);
|
context.read<TabsBloc>().openTab(widget.view);
|
||||||
break;
|
break;
|
||||||
|
case ViewMoreActionType.collapseAllPages:
|
||||||
|
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
||||||
|
break;
|
||||||
|
case ViewMoreActionType.changeIcon:
|
||||||
|
if (data is! EmojiPickerResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = data;
|
||||||
|
ViewBackendService.updateViewIcon(
|
||||||
|
viewId: widget.view.id,
|
||||||
|
viewIcon: result.emoji,
|
||||||
|
iconType: result.type.toProto(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw UnsupportedError('$action is not supported');
|
throw UnsupportedError('$action is not supported');
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// ··· button beside the view name
|
/// ··· button beside the view name
|
||||||
class ViewMoreActionButton extends StatelessWidget {
|
class ViewMoreActionButton extends StatelessWidget {
|
||||||
@ -14,59 +15,179 @@ class ViewMoreActionButton extends StatelessWidget {
|
|||||||
required this.view,
|
required this.view,
|
||||||
required this.onEditing,
|
required this.onEditing,
|
||||||
required this.onAction,
|
required this.onAction,
|
||||||
|
required this.spaceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final void Function(bool value) onEditing;
|
final void Function(bool value) onEditing;
|
||||||
final void Function(ViewMoreActionType) onAction;
|
final void Function(ViewMoreActionType type, dynamic data) onAction;
|
||||||
|
final FolderSpaceType spaceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final supportedActionTypes = [
|
final wrappers = _buildActionTypeWrappers();
|
||||||
ViewMoreActionType.rename,
|
|
||||||
ViewMoreActionType.delete,
|
|
||||||
ViewMoreActionType.duplicate,
|
|
||||||
ViewMoreActionType.openInNewTab,
|
|
||||||
view.isFavorite
|
|
||||||
? ViewMoreActionType.unFavorite
|
|
||||||
: ViewMoreActionType.favorite,
|
|
||||||
];
|
|
||||||
return PopoverActionList<ViewMoreActionTypeWrapper>(
|
return PopoverActionList<ViewMoreActionTypeWrapper>(
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
actions: supportedActionTypes
|
actions: wrappers,
|
||||||
.map((e) => ViewMoreActionTypeWrapper(e))
|
constraints: const BoxConstraints(
|
||||||
.toList(),
|
minWidth: 260,
|
||||||
|
),
|
||||||
buildChild: (popover) {
|
buildChild: (popover) {
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
hoverColor: Colors.transparent,
|
width: 24,
|
||||||
iconPadding: const EdgeInsets.all(2),
|
icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),
|
||||||
width: 26,
|
|
||||||
icon: const FlowySvg(FlowySvgs.details_s),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onEditing(true);
|
onEditing(true);
|
||||||
popover.show();
|
popover.show();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSelected: (action, popover) {
|
onSelected: (_, __) {},
|
||||||
onEditing(false);
|
|
||||||
onAction(action.inner);
|
|
||||||
popover.close();
|
|
||||||
},
|
|
||||||
onClosed: () => onEditing(false),
|
onClosed: () => onEditing(false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ViewMoreActionTypeWrapper> _buildActionTypeWrappers() {
|
||||||
|
final actionTypes = _buildActionTypes();
|
||||||
|
return actionTypes
|
||||||
|
.map(
|
||||||
|
(e) => ViewMoreActionTypeWrapper(e, (controller, data) {
|
||||||
|
onEditing(false);
|
||||||
|
onAction(e, data);
|
||||||
|
controller.close();
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ViewMoreActionType> _buildActionTypes() {
|
||||||
|
final List<ViewMoreActionType> actionTypes = [];
|
||||||
|
switch (spaceType) {
|
||||||
|
case FolderSpaceType.favorite:
|
||||||
|
actionTypes.addAll([
|
||||||
|
ViewMoreActionType.unFavorite,
|
||||||
|
ViewMoreActionType.divider,
|
||||||
|
ViewMoreActionType.rename,
|
||||||
|
ViewMoreActionType.openInNewTab,
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
actionTypes.addAll([
|
||||||
|
view.isFavorite
|
||||||
|
? ViewMoreActionType.unFavorite
|
||||||
|
: ViewMoreActionType.favorite,
|
||||||
|
ViewMoreActionType.divider,
|
||||||
|
ViewMoreActionType.rename,
|
||||||
|
ViewMoreActionType.changeIcon,
|
||||||
|
ViewMoreActionType.duplicate,
|
||||||
|
ViewMoreActionType.delete,
|
||||||
|
ViewMoreActionType.divider,
|
||||||
|
ViewMoreActionType.collapseAllPages,
|
||||||
|
ViewMoreActionType.divider,
|
||||||
|
ViewMoreActionType.openInNewTab,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return actionTypes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewMoreActionTypeWrapper extends ActionCell {
|
class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||||
ViewMoreActionTypeWrapper(this.inner);
|
ViewMoreActionTypeWrapper(this.inner, this.onTap);
|
||||||
|
|
||||||
final ViewMoreActionType inner;
|
final ViewMoreActionType inner;
|
||||||
|
final void Function(PopoverController controller, dynamic data) onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
|
Widget buildWithContext(BuildContext context, PopoverController controller) {
|
||||||
|
if (inner == ViewMoreActionType.divider) {
|
||||||
|
return _buildDivider();
|
||||||
|
} else if (inner == ViewMoreActionType.lastModified) {
|
||||||
|
return _buildLastModified(context);
|
||||||
|
} else if (inner == ViewMoreActionType.created) {
|
||||||
|
return _buildCreated(context);
|
||||||
|
} else if (inner == ViewMoreActionType.changeIcon) {
|
||||||
|
return _buildEmojiActionButton(context, controller);
|
||||||
|
} else {
|
||||||
|
return _buildNormalActionButton(context, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
Widget _buildNormalActionButton(
|
||||||
String get name => inner.name;
|
BuildContext context,
|
||||||
|
PopoverController controller,
|
||||||
|
) {
|
||||||
|
return _buildActionButton(context, () => onTap(controller, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiActionButton(
|
||||||
|
BuildContext context,
|
||||||
|
PopoverController controller,
|
||||||
|
) {
|
||||||
|
final child = _buildActionButton(context, null);
|
||||||
|
|
||||||
|
return AppFlowyPopover(
|
||||||
|
constraints: BoxConstraints.loose(const Size(364, 356)),
|
||||||
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
|
popupBuilder: (_) => FlowyIconPicker(
|
||||||
|
onSelected: (result) => onTap(controller, result),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDivider() {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Divider(height: 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLastModified(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 40,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCreated(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 40,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButton(
|
||||||
|
BuildContext context,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
height: 34,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
|
child: FlowyButton(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
leftIcon: inner.leftIcon,
|
||||||
|
rightIcon: inner.rightIcon,
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: SizedBox(
|
||||||
|
height: 18.0,
|
||||||
|
child: FlowyText.regular(
|
||||||
|
inner.name,
|
||||||
|
color: inner == ViewMoreActionType.delete
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class _FlowyTabState extends State<FlowyTab> {
|
|||||||
onExit: (_) => _setHovering(),
|
onExit: (_) => _setHovering(),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: HomeSizes.tabBarWidth,
|
width: HomeSizes.tabBarWidth,
|
||||||
height: HomeSizes.tabBarHeigth,
|
height: HomeSizes.tabBarHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getBackgroundColor(),
|
color: _getBackgroundColor(),
|
||||||
),
|
),
|
||||||
|
@ -57,7 +57,7 @@ class _TabsManagerState extends State<TabsManager>
|
|||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
alignment: Alignment.bottomLeft,
|
alignment: Alignment.bottomLeft,
|
||||||
height: HomeSizes.tabBarHeigth,
|
height: HomeSizes.tabBarHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
|
@ -48,10 +48,8 @@ class NotificationButton extends StatelessWidget {
|
|||||||
Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) {
|
Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
FlowySvg(
|
const FlowySvg(
|
||||||
FlowySvgs.clock_alarm_s,
|
FlowySvgs.notification_s,
|
||||||
size: const Size.square(24),
|
|
||||||
color: Theme.of(context).colorScheme.tertiary,
|
|
||||||
),
|
),
|
||||||
if (hasUnreads)
|
if (hasUnreads)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -23,6 +20,8 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
|
|||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SettingsAccountView extends StatefulWidget {
|
class SettingsAccountView extends StatefulWidget {
|
||||||
@ -342,7 +341,8 @@ class _UserProfileSettingState extends State<UserProfileSetting> {
|
|||||||
child: UserAvatar(
|
child: UserAvatar(
|
||||||
iconUrl: widget.iconUrl,
|
iconUrl: widget.iconUrl,
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
isLarge: true,
|
size: 48,
|
||||||
|
fontSize: 24,
|
||||||
isHovering: isHovering,
|
isHovering: isHovering,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
@ -43,6 +40,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -377,7 +376,8 @@ class _WorkspaceIconSetting extends StatelessWidget {
|
|||||||
child: WorkspaceIcon(
|
child: WorkspaceIcon(
|
||||||
workspace: workspace!,
|
workspace: workspace!,
|
||||||
iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20,
|
iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20,
|
||||||
enableEdit: enableEdit,
|
fontSize: 16.0,
|
||||||
|
enableEdit: true,
|
||||||
onSelected: (r) => context
|
onSelected: (r) => context
|
||||||
.read<WorkspaceSettingsBloc>()
|
.read<WorkspaceSettingsBloc>()
|
||||||
.add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)),
|
.add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)),
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FeatureFlagsPage extends StatelessWidget {
|
class FeatureFlagsPage extends StatelessWidget {
|
||||||
const FeatureFlagsPage({
|
const FeatureFlagsPage({
|
||||||
@ -50,7 +49,8 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> {
|
|||||||
subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),
|
subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),
|
||||||
trailing: Switch.adaptive(
|
trailing: Switch.adaptive(
|
||||||
value: widget.featureFlag.isOn,
|
value: widget.featureFlag.isOn,
|
||||||
onChanged: (value) => setState(() => widget.featureFlag.update(value)),
|
onChanged: (value) =>
|
||||||
|
setState(() async => widget.featureFlag.update(value)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||||
@ -16,7 +14,8 @@ import 'package:appflowy_result/appflowy_result.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class ViewFavoriteButton extends StatelessWidget {
|
class ViewFavoriteButton extends StatelessWidget {
|
||||||
@ -35,9 +33,8 @@ class ViewFavoriteButton extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s,
|
isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s,
|
||||||
size: const Size(18, 18),
|
size: const Size.square(18),
|
||||||
color: AFThemeExtension.of(context).warning,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -14,6 +11,8 @@ import 'package:flowy_infra/size.dart';
|
|||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
@ -147,7 +146,7 @@ class _DebugToast {
|
|||||||
|
|
||||||
class FlowyVersionDescription extends CustomActionCell {
|
class FlowyVersionDescription extends CustomActionCell {
|
||||||
@override
|
@override
|
||||||
Widget buildWithContext(BuildContext context) {
|
Widget buildWithContext(BuildContext context, PopoverController controller) {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: PackageInfo.fromPlatform(),
|
future: PackageInfo.fromPlatform(),
|
||||||
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
|
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
@ -14,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class MoreViewActions extends StatefulWidget {
|
class MoreViewActions extends StatefulWidget {
|
||||||
@ -101,8 +100,8 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
|||||||
builder: (context, isHovering) => Padding(
|
builder: (context, isHovering) => Padding(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: FlowySvg(
|
child: FlowySvg(
|
||||||
FlowySvgs.three_dots_vertical_s,
|
FlowySvgs.three_dots_s,
|
||||||
size: const Size.square(16),
|
size: const Size.square(18),
|
||||||
color: isHovering
|
color: isHovering
|
||||||
? Theme.of(context).colorScheme.onSecondary
|
? Theme.of(context).colorScheme.onSecondary
|
||||||
: Theme.of(context).iconTheme.color,
|
: Theme.of(context).iconTheme.color,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@ -83,7 +83,7 @@ class _PopoverActionListState<T extends PopoverAction>
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final custom = action as CustomActionCell;
|
final custom = action as CustomActionCell;
|
||||||
return custom.buildWithContext(context);
|
return custom.buildWithContext(context, popoverController);
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ abstract class PopoverActionCell extends PopoverAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class CustomActionCell extends PopoverAction {
|
abstract class CustomActionCell extends PopoverAction {
|
||||||
Widget buildWithContext(BuildContext context);
|
Widget buildWithContext(BuildContext context, PopoverController controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class PopoverAction {}
|
abstract class PopoverAction {}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class RenameViewPopover extends StatefulWidget {
|
class RenameViewPopover extends StatefulWidget {
|
||||||
const RenameViewPopover({
|
const RenameViewPopover({
|
||||||
@ -51,17 +51,20 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (widget.showIconChanger) ...[
|
if (widget.showIconChanger) ...[
|
||||||
EmojiPickerButton(
|
SizedBox(
|
||||||
|
width: 30.0,
|
||||||
|
child: EmojiPickerButton(
|
||||||
emoji: widget.emoji,
|
emoji: widget.emoji,
|
||||||
defaultIcon: widget.icon,
|
defaultIcon: widget.icon,
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
offset: const Offset(0, 18),
|
offset: const Offset(0, 18),
|
||||||
onSubmitted: _updateViewIcon,
|
onSubmitted: _updateViewIcon,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const HSpace(6),
|
const HSpace(6),
|
||||||
],
|
],
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 36.0,
|
height: 32.0,
|
||||||
width: 220,
|
width: 220,
|
||||||
child: FlowyTextField(
|
child: FlowyTextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
@ -1,37 +1,32 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/util/built_in_svgs.dart';
|
import 'package:appflowy/util/built_in_svgs.dart';
|
||||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
const double _smallSize = 28;
|
|
||||||
const double _largeSize = 64;
|
|
||||||
|
|
||||||
class UserAvatar extends StatelessWidget {
|
class UserAvatar extends StatelessWidget {
|
||||||
const UserAvatar({
|
const UserAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.iconUrl,
|
required this.iconUrl,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.isLarge = false,
|
required this.size,
|
||||||
|
required this.fontSize,
|
||||||
this.isHovering = false,
|
this.isHovering = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String iconUrl;
|
final String iconUrl;
|
||||||
final String name;
|
final String name;
|
||||||
final bool isLarge;
|
final double size;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
// If true, a border will be applied on top of the avatar
|
// If true, a border will be applied on top of the avatar
|
||||||
final bool isHovering;
|
final bool isHovering;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = isLarge ? _largeSize : _smallSize;
|
|
||||||
|
|
||||||
if (iconUrl.isEmpty) {
|
if (iconUrl.isEmpty) {
|
||||||
final String nameOrDefault = _userName(name);
|
final String nameOrDefault = _userName(name);
|
||||||
final Color color = ColorGenerator(name).toColor();
|
final Color color = ColorGenerator(name).toColor();
|
||||||
@ -59,16 +54,10 @@ class UserAvatar extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: FlowyText.semibold(
|
child: FlowyText.regular(
|
||||||
nameInitials,
|
nameInitials,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
fontSize: isLarge
|
fontSize: fontSize,
|
||||||
? nameInitials.length == initialsCount
|
|
||||||
? 20
|
|
||||||
: 26
|
|
||||||
: nameInitials.length == initialsCount
|
|
||||||
? 12
|
|
||||||
: 14,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -94,7 +83,7 @@ class UserAvatar extends StatelessWidget {
|
|||||||
FlowySvgData('emoji/$iconUrl'),
|
FlowySvgData('emoji/$iconUrl'),
|
||||||
blendMode: null,
|
blendMode: null,
|
||||||
)
|
)
|
||||||
: EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18),
|
: FlowyText.emoji(iconUrl, fontSize: fontSize),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
import 'dart:math';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
|
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
// workspace name / ... / view_title
|
// workspace name > ... > view_title
|
||||||
class ViewTitleBar extends StatefulWidget {
|
class ViewTitleBar extends StatelessWidget {
|
||||||
const ViewTitleBar({
|
const ViewTitleBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
@ -25,133 +20,83 @@ class ViewTitleBar extends StatefulWidget {
|
|||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
|
|
||||||
@override
|
// late Future<List<ViewPB>> ancestors;
|
||||||
State<ViewTitleBar> createState() => _ViewTitleBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ViewTitleBarState extends State<ViewTitleBar> {
|
|
||||||
late Future<List<ViewPB>> ancestors;
|
|
||||||
late String viewId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
viewId = widget.view.id;
|
|
||||||
_reloadAncestors(viewId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant ViewTitleBar oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
|
|
||||||
if (oldWidget.view.id != widget.view.id) {
|
|
||||||
viewId = widget.view.id;
|
|
||||||
_reloadAncestors(viewId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<ViewPB>>(
|
return BlocProvider(
|
||||||
future: ancestors,
|
create: (_) =>
|
||||||
builder: (context, snapshot) {
|
ViewTitleBarBloc(view: view)..add(const ViewTitleBarEvent.initial()),
|
||||||
final ancestors = snapshot.data;
|
child: BlocBuilder<ViewTitleBarBloc, ViewTitleBarState>(
|
||||||
if (ancestors == null ||
|
builder: (context, state) {
|
||||||
snapshot.connectionState != ConnectionState.done) {
|
final ancestors = state.ancestors;
|
||||||
|
if (ancestors.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
const maxWidth = WindowSizeManager.minWindowWidth / 2.0;
|
return SingleChildScrollView(
|
||||||
final replacement = Row(
|
child: SizedBox(
|
||||||
// refresh the view title bar when the ancestors changed
|
height: 24,
|
||||||
key: ValueKey(ancestors.hashCode),
|
child: Row(children: _buildViewTitles(context, ancestors)),
|
||||||
children: _buildViewTitles(context, ancestors),
|
|
||||||
);
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return Visibility(
|
|
||||||
visible: constraints.maxWidth < maxWidth,
|
|
||||||
replacement: replacement,
|
|
||||||
// if the width is too small, only show one view title bar without the ancestors
|
|
||||||
child: _ViewTitle(
|
|
||||||
key: ValueKey(ancestors.last),
|
|
||||||
view: ancestors.last,
|
|
||||||
maxTitleWidth: constraints.maxWidth,
|
|
||||||
onUpdated: () => setState(() => _reloadAncestors(viewId)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildViewTitles(BuildContext context, List<ViewPB> views) {
|
List<Widget> _buildViewTitles(BuildContext context, List<ViewPB> views) {
|
||||||
// if the level is too deep, only show the last two view, the first one view and the root view
|
// if the level is too deep, only show the last two view, the first one view and the root view
|
||||||
|
// for example:
|
||||||
|
// if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5]
|
||||||
|
// if the views are [root, view1, view2, view3], show [root, view1, view2, view3]
|
||||||
|
const lowerBound = 2;
|
||||||
|
final upperBound = views.length - 2;
|
||||||
bool hasAddedEllipsis = false;
|
bool hasAddedEllipsis = false;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
|
|
||||||
for (var i = 0; i < views.length; i++) {
|
if (views.length <= 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the workspace name, use section name instead in the future
|
||||||
|
// skip the workspace view
|
||||||
|
for (var i = 1; i < views.length; i++) {
|
||||||
final view = views[i];
|
final view = views[i];
|
||||||
|
|
||||||
if (i >= 1 && i < views.length - 2) {
|
if (i >= lowerBound && i < upperBound) {
|
||||||
if (!hasAddedEllipsis) {
|
if (!hasAddedEllipsis) {
|
||||||
hasAddedEllipsis = true;
|
hasAddedEllipsis = true;
|
||||||
children.add(
|
children.addAll([
|
||||||
const FlowyText.regular(' ... /'),
|
const FlowyText.regular(' ... '),
|
||||||
);
|
const FlowySvg(FlowySvgs.title_bar_divider_s),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget child;
|
final child = FlowyTooltip(
|
||||||
if (i == 0) {
|
|
||||||
final currentWorkspace =
|
|
||||||
context.read<UserWorkspaceBloc>().state.currentWorkspace;
|
|
||||||
final icon = currentWorkspace?.icon ?? '';
|
|
||||||
final name = currentWorkspace?.name ?? view.name;
|
|
||||||
// the first one is the workspace name
|
|
||||||
child = FlowyTooltip(
|
|
||||||
message: name,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
EmojiText(
|
|
||||||
emoji: icon,
|
|
||||||
fontSize: 18.0,
|
|
||||||
),
|
|
||||||
const HSpace(2.0),
|
|
||||||
FlowyText.regular(name),
|
|
||||||
const HSpace(4.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
child = FlowyTooltip(
|
|
||||||
message: view.name,
|
message: view.name,
|
||||||
child: _ViewTitle(
|
child: _ViewTitle(
|
||||||
view: view,
|
view: view,
|
||||||
behavior: i == views.length - 1
|
behavior: i == views.length - 1
|
||||||
? _ViewTitleBehavior.editable // only the last one is editable
|
? _ViewTitleBehavior.editable // only the last one is editable
|
||||||
: _ViewTitleBehavior.uneditable, // others are not editable
|
: _ViewTitleBehavior.uneditable, // others are not editable
|
||||||
onUpdated: () => setState(() => _reloadAncestors(viewId)),
|
onUpdated: () {
|
||||||
|
context
|
||||||
|
.read<ViewTitleBarBloc>()
|
||||||
|
.add(const ViewTitleBarEvent.reload());
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
children.add(child);
|
children.add(child);
|
||||||
|
|
||||||
if (i != views.length - 1) {
|
if (i != views.length - 1) {
|
||||||
// if not the last one, add a divider
|
// if not the last one, add a divider
|
||||||
children.add(const FlowyText.regular('/'));
|
children.add(const FlowySvg(FlowySvgs.title_bar_divider_s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reloadAncestors(String viewId) {
|
|
||||||
ancestors = ViewBackendService.getViewAncestors(viewId)
|
|
||||||
.fold((s) => s.items, (f) => []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _ViewTitleBehavior {
|
enum _ViewTitleBehavior {
|
||||||
@ -161,16 +106,13 @@ enum _ViewTitleBehavior {
|
|||||||
|
|
||||||
class _ViewTitle extends StatefulWidget {
|
class _ViewTitle extends StatefulWidget {
|
||||||
const _ViewTitle({
|
const _ViewTitle({
|
||||||
super.key,
|
|
||||||
required this.view,
|
required this.view,
|
||||||
this.behavior = _ViewTitleBehavior.editable,
|
this.behavior = _ViewTitleBehavior.editable,
|
||||||
this.maxTitleWidth = 180,
|
|
||||||
required this.onUpdated,
|
required this.onUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final _ViewTitleBehavior behavior;
|
final _ViewTitleBehavior behavior;
|
||||||
final double maxTitleWidth;
|
|
||||||
final VoidCallback onUpdated;
|
final VoidCallback onUpdated;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -180,87 +122,58 @@ class _ViewTitle extends StatefulWidget {
|
|||||||
class _ViewTitleState extends State<_ViewTitle> {
|
class _ViewTitleState extends State<_ViewTitle> {
|
||||||
final popoverController = PopoverController();
|
final popoverController = PopoverController();
|
||||||
final textEditingController = TextEditingController();
|
final textEditingController = TextEditingController();
|
||||||
late final viewListener = ViewListener(viewId: widget.view.id);
|
|
||||||
|
|
||||||
String name = '';
|
|
||||||
String icon = '';
|
|
||||||
String inputtingName = '';
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
name = widget.view.name;
|
|
||||||
icon = widget.view.icon.value;
|
|
||||||
|
|
||||||
_resetTextEditingController();
|
|
||||||
viewListener.start(
|
|
||||||
onViewUpdated: (view) {
|
|
||||||
if (name != view.name || icon != view.icon.value) {
|
|
||||||
widget.onUpdated();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
name = view.name;
|
|
||||||
icon = view.icon.value;
|
|
||||||
_resetTextEditingController();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
textEditingController.dispose();
|
textEditingController.dispose();
|
||||||
popoverController.close();
|
popoverController.close();
|
||||||
viewListener.stop();
|
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isEditable = widget.behavior == _ViewTitleBehavior.editable;
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()),
|
||||||
|
child: BlocConsumer<ViewTitleBloc, ViewTitleState>(
|
||||||
|
listener: (_, state) {
|
||||||
|
_resetTextEditingController(state);
|
||||||
|
widget.onUpdated();
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
// root view
|
// root view
|
||||||
if (widget.view.parentViewId.isEmpty) {
|
if (widget.view.parentViewId.isEmpty) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
FlowyText.regular(name),
|
FlowyText.regular(state.name),
|
||||||
const HSpace(4.0),
|
const HSpace(4.0),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
} else if (isEditable) {
|
||||||
|
return _buildEditableViewTitle(context, state);
|
||||||
|
} else {
|
||||||
|
return _buildUnEditableViewTitle(context, state);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
final child = SingleChildScrollView(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
EmojiText(
|
|
||||||
emoji: icon,
|
|
||||||
fontSize: 18.0,
|
|
||||||
),
|
|
||||||
const HSpace(2.0),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: max(0, widget.maxTitleWidth),
|
|
||||||
),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
name,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.behavior == _ViewTitleBehavior.uneditable) {
|
Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),
|
onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
text: child,
|
text: _buildIconAndName(state),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: 300,
|
maxWidth: 300,
|
||||||
@ -268,32 +181,55 @@ class _ViewTitleState extends State<_ViewTitle> {
|
|||||||
),
|
),
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
offset: const Offset(0, 18),
|
offset: const Offset(0, 6),
|
||||||
popupBuilder: (context) {
|
popupBuilder: (context) {
|
||||||
// icon + textfield
|
// icon + textfield
|
||||||
_resetTextEditingController();
|
_resetTextEditingController(state);
|
||||||
return RenameViewPopover(
|
return RenameViewPopover(
|
||||||
viewId: widget.view.id,
|
viewId: widget.view.id,
|
||||||
name: widget.view.name,
|
name: widget.view.name,
|
||||||
popoverController: popoverController,
|
popoverController: popoverController,
|
||||||
icon: widget.view.defaultIcon(),
|
icon: widget.view.defaultIcon(),
|
||||||
emoji: icon,
|
emoji: state.icon,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
useIntrinsicWidth: true,
|
useIntrinsicWidth: true,
|
||||||
text: child,
|
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
|
text: _buildIconAndName(state),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetTextEditingController() {
|
Widget _buildIconAndName(ViewTitleState state) {
|
||||||
inputtingName = name;
|
return SingleChildScrollView(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (state.icon.isNotEmpty) ...[
|
||||||
|
FlowyText.emoji(
|
||||||
|
state.icon,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
const HSpace(6.0),
|
||||||
|
],
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
state.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetTextEditingController(ViewTitleState state) {
|
||||||
textEditingController
|
textEditingController
|
||||||
..text = name
|
..text = state.name
|
||||||
..selection = TextSelection(
|
..selection = TextSelection(
|
||||||
baseOffset: 0,
|
baseOffset: 0,
|
||||||
extentOffset: name.length,
|
extentOffset: state.name.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
|
||||||
private let kTrafficLightOffetTop = 22
|
private let kTrafficLightOffetTop = 14
|
||||||
|
|
||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
func registerMethodChannel(flutterViewController: FlutterViewController) {
|
func registerMethodChannel(flutterViewController: FlutterViewController) {
|
||||||
@ -51,9 +51,9 @@ class MainFlutterWindow: NSWindow {
|
|||||||
let zoomButton = self.standardWindowButton(ButtonType.zoomButton)!
|
let zoomButton = self.standardWindowButton(ButtonType.zoomButton)!
|
||||||
let titlebarView = closeButton.superview!
|
let titlebarView = closeButton.superview!
|
||||||
|
|
||||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20)
|
self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12)
|
||||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38)
|
self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30)
|
||||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56)
|
self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48)
|
||||||
|
|
||||||
let customToolbar = NSTitlebarAccessoryViewController()
|
let customToolbar = NSTitlebarAccessoryViewController()
|
||||||
let newView = NSView()
|
let newView = NSView()
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
import 'package:appflowy_popover/src/layout.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy_popover/src/layout.dart';
|
|
||||||
|
|
||||||
import 'mask.dart';
|
import 'mask.dart';
|
||||||
import 'mutex.dart';
|
import 'mutex.dart';
|
||||||
|
|
||||||
@ -291,6 +290,13 @@ class PopoverContainer extends StatefulWidget {
|
|||||||
context.findAncestorStateOfType<PopoverContainerState>();
|
context.findAncestorStateOfType<PopoverContainerState>();
|
||||||
return result!;
|
return result!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PopoverContainerState? maybeOf(BuildContext context) {
|
||||||
|
if (context is StatefulElement && context.state is PopoverContainerState) {
|
||||||
|
return context.state as PopoverContainerState;
|
||||||
|
}
|
||||||
|
return context.findAncestorStateOfType<PopoverContainerState>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopoverContainerState extends State<PopoverContainer> {
|
class PopoverContainerState extends State<PopoverContainer> {
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// MARK: - Shared Builder
|
// MARK: - Shared Builder
|
||||||
|
|
||||||
typedef WidgetBuilder = Widget Function();
|
|
||||||
|
|
||||||
typedef IndexedCallback = void Function(int index);
|
typedef IndexedCallback = void Function(int index);
|
||||||
typedef IndexedValueCallback<T> = void Function(T value, int index);
|
typedef IndexedValueCallback<T> = void Function(T value, int index);
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowyButton extends StatelessWidget {
|
class FlowyButton extends StatelessWidget {
|
||||||
final Widget text;
|
final Widget text;
|
||||||
|
@ -7,10 +7,11 @@ class FlowyDecoration {
|
|||||||
double spreadRadius = 0,
|
double spreadRadius = 0,
|
||||||
double blurRadius = 20,
|
double blurRadius = 20,
|
||||||
Offset offset = Offset.zero,
|
Offset offset = Offset.zero,
|
||||||
|
double borderRadius = 6,
|
||||||
}) {
|
}) {
|
||||||
return BoxDecoration(
|
return BoxDecoration(
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: boxShadow,
|
color: boxShadow,
|
||||||
|
@ -16,6 +16,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
final List<String>? fallbackFontFamily;
|
final List<String>? fallbackFontFamily;
|
||||||
final double? lineHeight;
|
final double? lineHeight;
|
||||||
final bool withTooltip;
|
final bool withTooltip;
|
||||||
|
final StrutStyle? strutStyle;
|
||||||
|
|
||||||
const FlowyText(
|
const FlowyText(
|
||||||
this.text, {
|
this.text, {
|
||||||
@ -32,6 +33,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fallbackFontFamily,
|
this.fallbackFontFamily,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
FlowyText.small(
|
FlowyText.small(
|
||||||
@ -47,6 +49,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fallbackFontFamily,
|
this.fallbackFontFamily,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle,
|
||||||
}) : fontWeight = FontWeight.w400,
|
}) : fontWeight = FontWeight.w400,
|
||||||
fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12;
|
fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12;
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fallbackFontFamily,
|
this.fallbackFontFamily,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle,
|
||||||
}) : fontWeight = FontWeight.w400;
|
}) : fontWeight = FontWeight.w400;
|
||||||
|
|
||||||
const FlowyText.medium(
|
const FlowyText.medium(
|
||||||
@ -80,6 +84,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fallbackFontFamily,
|
this.fallbackFontFamily,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle,
|
||||||
}) : fontWeight = FontWeight.w500;
|
}) : fontWeight = FontWeight.w500;
|
||||||
|
|
||||||
const FlowyText.semibold(
|
const FlowyText.semibold(
|
||||||
@ -96,6 +101,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fallbackFontFamily,
|
this.fallbackFontFamily,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle,
|
||||||
}) : fontWeight = FontWeight.w600;
|
}) : fontWeight = FontWeight.w600;
|
||||||
|
|
||||||
// Some emojis are not supported on Linux and Android, fallback to noto color emoji
|
// Some emojis are not supported on Linux and Android, fallback to noto color emoji
|
||||||
@ -105,12 +111,13 @@ class FlowyText extends StatelessWidget {
|
|||||||
this.fontSize,
|
this.fontSize,
|
||||||
this.overflow,
|
this.overflow,
|
||||||
this.color,
|
this.color,
|
||||||
this.textAlign,
|
this.textAlign = TextAlign.center,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
this.decoration,
|
this.decoration,
|
||||||
this.selectable = false,
|
this.selectable = false,
|
||||||
this.lineHeight,
|
this.lineHeight,
|
||||||
this.withTooltip = false,
|
this.withTooltip = false,
|
||||||
|
this.strutStyle = const StrutStyle(forceStrutHeight: true),
|
||||||
}) : fontWeight = FontWeight.w400,
|
}) : fontWeight = FontWeight.w400,
|
||||||
fontFamily = 'noto color emoji',
|
fontFamily = 'noto color emoji',
|
||||||
fallbackFontFamily = null;
|
fallbackFontFamily = null;
|
||||||
@ -119,12 +126,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child;
|
Widget child;
|
||||||
|
|
||||||
if (selectable) {
|
final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
child = SelectableText(
|
|
||||||
text,
|
|
||||||
maxLines: maxLines,
|
|
||||||
textAlign: textAlign,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
fontWeight: fontWeight,
|
fontWeight: fontWeight,
|
||||||
color: color,
|
color: color,
|
||||||
@ -132,7 +134,15 @@ class FlowyText extends StatelessWidget {
|
|||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
fontFamilyFallback: fallbackFontFamily,
|
fontFamilyFallback: fallbackFontFamily,
|
||||||
height: lineHeight,
|
height: lineHeight,
|
||||||
),
|
);
|
||||||
|
|
||||||
|
if (selectable) {
|
||||||
|
child = SelectableText(
|
||||||
|
text,
|
||||||
|
maxLines: maxLines,
|
||||||
|
textAlign: textAlign,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
style: textStyle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = Text(
|
child = Text(
|
||||||
@ -140,15 +150,7 @@ class FlowyText extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
overflow: overflow ?? TextOverflow.clip,
|
overflow: overflow ?? TextOverflow.clip,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
style: textStyle,
|
||||||
fontSize: fontSize,
|
|
||||||
fontWeight: fontWeight,
|
|
||||||
color: color,
|
|
||||||
decoration: decoration,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
fontFamilyFallback: fallbackFontFamily,
|
|
||||||
height: lineHeight,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
frontend/resources/flowy_icons/16x/add_cover.svg
Normal file
7
frontend/resources/flowy_icons/16x/add_cover.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g opacity="0.3">
|
||||||
|
<path d="M1.72052 13.1735L1.70719 13.1868C1.52719 12.7935 1.41385 12.3468 1.36719 11.8535C1.41385 12.3402 1.54052 12.7802 1.72052 13.1735Z" fill="#171717"/>
|
||||||
|
<path d="M6.00073 7.41943C6.87702 7.41943 7.5874 6.70905 7.5874 5.83276C7.5874 4.95647 6.87702 4.24609 6.00073 4.24609C5.12444 4.24609 4.41406 4.95647 4.41406 5.83276C4.41406 6.70905 5.12444 7.41943 6.00073 7.41943Z" fill="#171717"/>
|
||||||
|
<path d="M10.792 1.83398H5.20536C2.7787 1.83398 1.33203 3.28065 1.33203 5.70732V11.294C1.33203 12.0207 1.4587 12.654 1.70536 13.1873C2.2787 14.454 3.50536 15.1673 5.20536 15.1673H10.792C13.2187 15.1673 14.6654 13.7207 14.6654 11.294V9.76732V5.70732C14.6654 3.28065 13.2187 1.83398 10.792 1.83398ZM13.5787 8.83398C13.0587 8.38732 12.2187 8.38732 11.6987 8.83398L8.92537 11.214C8.40537 11.6607 7.56536 11.6607 7.04536 11.214L6.8187 11.0273C6.34536 10.614 5.59203 10.574 5.0587 10.934L2.56536 12.6073C2.4187 12.234 2.33203 11.8007 2.33203 11.294V5.70732C2.33203 3.82732 3.32536 2.83398 5.20536 2.83398H10.792C12.672 2.83398 13.6654 3.82732 13.6654 5.70732V8.90732L13.5787 8.83398Z" fill="#171717"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
5
frontend/resources/flowy_icons/16x/add_icon.svg
Normal file
5
frontend/resources/flowy_icons/16x/add_icon.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g opacity="0.3">
|
||||||
|
<path d="M12.739 3.80656C10.1323 1.1999 5.899 1.1999 3.29233 3.80656C0.639001 6.4599 0.685667 10.7866 3.42567 13.3866C5.959 15.7799 10.0657 15.7799 12.599 13.3866C15.3457 10.7866 15.3923 6.4599 12.739 3.80656ZM10.919 11.5999C10.119 12.3599 9.06567 12.7399 8.01233 12.7399C6.959 12.7399 5.90567 12.3599 5.10567 11.5999C4.90567 11.4066 4.899 11.0932 5.08567 10.8932C5.279 10.6932 5.59233 10.6866 5.79233 10.8732C7.01233 12.0266 9.00567 12.0332 10.2323 10.8732C10.4323 10.6866 10.7523 10.6932 10.939 10.8932C11.1323 11.0932 11.119 11.4066 10.919 11.5999Z" fill="#171717"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 696 B |
6
frontend/resources/flowy_icons/16x/add_workspace.svg
Normal file
6
frontend/resources/flowy_icons/16x/add_workspace.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g opacity="0.5">
|
||||||
|
<rect x="2" y="7.3999" width="12" height="1.2" rx="0.6" fill="#171717"/>
|
||||||
|
<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 309 B |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user