mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: display the titles of a view's ancestors and the view's title on the title bar. (#3898)
* feat: add no pages inside tips * feat: show view's ancestors (include itself) title on bar * feat: show view's ancestors (include itself) title on bar * test: add integration tests * fix: integration tests
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:emoji_mart/emoji_mart.dart';
|
import 'package:emoji_mart/emoji_mart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -155,7 +156,11 @@ void main() {
|
|||||||
const hand = '👋🏿';
|
const hand = '👋🏿';
|
||||||
await tester.tapEmoji(hand);
|
await tester.tapEmoji(hand);
|
||||||
tester.expectToSeeDocumentIcon(hand);
|
tester.expectToSeeDocumentIcon(hand);
|
||||||
tester.isPageWithIcon(gettingStarted, hand);
|
tester.expectViewHasIcon(
|
||||||
|
gettingStarted,
|
||||||
|
ViewLayoutPB.Document,
|
||||||
|
hand,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ void main() {
|
|||||||
await tester.favoriteViewByName(names[1]);
|
await tester.favoriteViewByName(names[1]);
|
||||||
expect(
|
expect(
|
||||||
tester.findFavoritePageName(names[1]),
|
tester.findFavoritePageName(names[1]),
|
||||||
findsNWidgets(1),
|
findsNWidgets(2),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.unfavoriteViewByName(gettingStarted);
|
await tester.unfavoriteViewByName(gettingStarted);
|
||||||
@ -99,7 +99,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester.findFavoritePageName(name),
|
tester.findFavoritePageName(name),
|
||||||
findsNothing,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -127,11 +127,11 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.byWidgetPredicate(
|
find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is ViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
widget.categoryType == FolderCategoryType.favorite,
|
||||||
),
|
),
|
||||||
findsNWidgets(3),
|
findsNWidgets(6),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.hoverOnPageName(
|
await tester.hoverOnPageName(
|
||||||
@ -144,13 +144,8 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find.byWidgetPredicate(
|
tester.findAllFavoritePages(),
|
||||||
(widget) =>
|
findsNWidgets(3),
|
||||||
widget is ViewItem &&
|
|
||||||
widget.view.isFavorite &&
|
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
|
||||||
),
|
|
||||||
findsNWidgets(2),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.hoverOnPageName(
|
await tester.hoverOnPageName(
|
||||||
@ -163,12 +158,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find.byWidgetPredicate(
|
tester.findAllFavoritePages(),
|
||||||
(widget) =>
|
|
||||||
widget is ViewItem &&
|
|
||||||
widget.view.isFavorite &&
|
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
|
||||||
),
|
|
||||||
findsNothing,
|
findsNothing,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import '../util/base.dart';
|
||||||
|
import '../util/common_operations.dart';
|
||||||
|
import '../util/expectation.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
const emoji = '😁';
|
||||||
|
|
||||||
|
group('Icon', () {
|
||||||
|
testWidgets('Update page icon in sidebar', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// create document, board, grid and calendar views
|
||||||
|
for (final value in ViewLayoutPB.values) {
|
||||||
|
await tester.createNewPageWithName(
|
||||||
|
name: value.name,
|
||||||
|
parentName: gettingStarted,
|
||||||
|
layout: value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// update its icon
|
||||||
|
await tester.updatePageIconInSidebarByName(
|
||||||
|
name: value.name,
|
||||||
|
parentName: gettingStarted,
|
||||||
|
layout: value,
|
||||||
|
icon: emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.expectViewHasIcon(
|
||||||
|
value.name,
|
||||||
|
value,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Update page icon in title bar', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// create document, board, grid and calendar views
|
||||||
|
for (final value in ViewLayoutPB.values) {
|
||||||
|
await tester.createNewPageWithName(
|
||||||
|
name: value.name,
|
||||||
|
parentName: gettingStarted,
|
||||||
|
layout: value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// update its icon
|
||||||
|
await tester.updatePageIconInTitleBarByName(
|
||||||
|
name: value.name,
|
||||||
|
layout: value,
|
||||||
|
icon: emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.expectViewHasIcon(
|
||||||
|
value.name,
|
||||||
|
value,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.expectViewTitleHasIcon(
|
||||||
|
value.name,
|
||||||
|
value,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'sidebar_test.dart' as sidebar_test;
|
|
||||||
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
|
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
|
||||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||||
|
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||||
|
import 'sidebar_test.dart' as sidebar_test;
|
||||||
|
|
||||||
void startTesting() {
|
void startTesting() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -11,4 +12,5 @@ void startTesting() {
|
|||||||
sidebar_test.main();
|
sidebar_test.main();
|
||||||
sidebar_expanded_test.main();
|
sidebar_expanded_test.main();
|
||||||
sidebar_favorite_test.main();
|
sidebar_favorite_test.main();
|
||||||
|
sidebar_icon_test.main();
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ 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';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
import 'package:appflowy/plugins/document/presentation/share/share_button.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';
|
||||||
@ -14,6 +15,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.
|
|||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -23,6 +25,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'emoji.dart';
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
|
|
||||||
extension CommonOperations on WidgetTester {
|
extension CommonOperations on WidgetTester {
|
||||||
@ -442,6 +445,47 @@ extension CommonOperations on WidgetTester {
|
|||||||
);
|
);
|
||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the page icon in the sidebar
|
||||||
|
Future<void> updatePageIconInSidebarByName({
|
||||||
|
required String name,
|
||||||
|
required String parentName,
|
||||||
|
required ViewLayoutPB layout,
|
||||||
|
required String icon,
|
||||||
|
}) async {
|
||||||
|
final iconButton = find.descendant(
|
||||||
|
of: findPageName(
|
||||||
|
name,
|
||||||
|
layout: layout,
|
||||||
|
parentName: parentName,
|
||||||
|
),
|
||||||
|
matching:
|
||||||
|
find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),
|
||||||
|
);
|
||||||
|
await tapButton(iconButton);
|
||||||
|
await tapEmoji(icon);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the page icon in the sidebar
|
||||||
|
Future<void> updatePageIconInTitleBarByName({
|
||||||
|
required String name,
|
||||||
|
required ViewLayoutPB layout,
|
||||||
|
required String icon,
|
||||||
|
}) async {
|
||||||
|
await openPage(
|
||||||
|
name,
|
||||||
|
layout: layout,
|
||||||
|
);
|
||||||
|
final title = find.descendant(
|
||||||
|
of: find.byType(ViewTitleBar),
|
||||||
|
matching: find.text(name),
|
||||||
|
);
|
||||||
|
await tapButton(title);
|
||||||
|
await tapButton(find.byType(EmojiPickerButton));
|
||||||
|
await tapEmoji(icon);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
|
import 'package:emoji_mart/emoji_mart.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
|
||||||
extension EmojiTestExtension on WidgetTester {
|
extension EmojiTestExtension on WidgetTester {
|
||||||
Future<void> tapEmoji(String emoji) async {
|
Future<void> tapEmoji(String emoji) async {
|
||||||
final emojiWidget = find.text(emoji);
|
final emojiWidget = find.descendant(
|
||||||
|
of: find.byType(EmojiPicker),
|
||||||
|
matching: find.text(emoji),
|
||||||
|
);
|
||||||
await tapButton(emojiWidget);
|
await tapButton(emojiWidget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
|
|||||||
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/home_stack.dart';
|
import 'package:appflowy/workspace/presentation/home/home_stack.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/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/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';
|
||||||
@ -164,7 +165,7 @@ extension Expectation on WidgetTester {
|
|||||||
}) {
|
}) {
|
||||||
return find.byWidgetPredicate(
|
return find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is ViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite &&
|
widget.categoryType == FolderCategoryType.favorite &&
|
||||||
widget.view.name == name &&
|
widget.view.name == name &&
|
||||||
@ -173,6 +174,15 @@ extension Expectation on WidgetTester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Finder findAllFavoritePages() {
|
||||||
|
return find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is SingleInnerViewItem &&
|
||||||
|
widget.view.isFavorite &&
|
||||||
|
widget.categoryType == FolderCategoryType.favorite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Finder findPageName(
|
Finder findPageName(
|
||||||
String name, {
|
String name, {
|
||||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||||
@ -201,12 +211,23 @@ extension Expectation on WidgetTester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void isPageWithIcon(String name, String emoji) {
|
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||||
final pageName = findPageName(name);
|
final pageName = findPageName(
|
||||||
|
name,
|
||||||
|
layout: layout,
|
||||||
|
);
|
||||||
final icon = find.descendant(
|
final icon = find.descendant(
|
||||||
of: pageName,
|
of: pageName,
|
||||||
matching: find.text(emoji),
|
matching: find.text(emoji),
|
||||||
);
|
);
|
||||||
expect(icon, findsOneWidget);
|
expect(icon, findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||||
|
final icon = find.descendant(
|
||||||
|
of: find.byType(ViewTitleBar),
|
||||||
|
matching: find.text(emoji),
|
||||||
|
);
|
||||||
|
expect(icon, findsOneWidget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import 'package:appflowy/plugins/database_view/widgets/share_button.dart';
|
|||||||
import 'package:appflowy/plugins/util.dart';
|
import 'package:appflowy/plugins/util.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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';
|
||||||
@ -190,7 +190,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
Widget get leftBarItem => ViewTitleBar(view: notifier.view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
|
@ -9,10 +9,10 @@ import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
|||||||
import 'package:appflowy/plugins/util.dart';
|
import 'package:appflowy/plugins/util.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.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';
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
Widget get leftBarItem => ViewTitleBar(view: view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
|
@ -12,8 +12,11 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.emoji,
|
required this.emoji,
|
||||||
required this.onSubmitted,
|
required this.onSubmitted,
|
||||||
this.emojiPickerSize = const Size(300, 250),
|
this.emojiPickerSize = const Size(360, 380),
|
||||||
this.emojiSize = 18.0,
|
this.emojiSize = 18.0,
|
||||||
|
this.defaultIcon,
|
||||||
|
this.offset,
|
||||||
|
this.direction,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String emoji;
|
final String emoji;
|
||||||
@ -21,6 +24,9 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
final Size emojiPickerSize;
|
final Size emojiPickerSize;
|
||||||
final void Function(String emoji, PopoverController? controller) onSubmitted;
|
final void Function(String emoji, PopoverController? controller) onSubmitted;
|
||||||
final PopoverController popoverController = PopoverController();
|
final PopoverController popoverController = PopoverController();
|
||||||
|
final Widget? defaultIcon;
|
||||||
|
final Offset? offset;
|
||||||
|
final PopoverDirection? direction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -32,6 +38,8 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
width: emojiPickerSize.width,
|
width: emojiPickerSize.width,
|
||||||
height: emojiPickerSize.height,
|
height: emojiPickerSize.height,
|
||||||
),
|
),
|
||||||
|
offset: offset,
|
||||||
|
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
||||||
popupBuilder: (context) => Container(
|
popupBuilder: (context) => Container(
|
||||||
width: emojiPickerSize.width,
|
width: emojiPickerSize.width,
|
||||||
height: emojiPickerSize.height,
|
height: emojiPickerSize.height,
|
||||||
@ -41,7 +49,13 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
onExit: () {},
|
onExit: () {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: FlowyTextButton(
|
child: emoji.isEmpty && defaultIcon != null
|
||||||
|
? FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
text: defaultIcon!,
|
||||||
|
onTap: () => popoverController.show(),
|
||||||
|
)
|
||||||
|
: FlowyTextButton(
|
||||||
emoji,
|
emoji,
|
||||||
overflow: TextOverflow.visible,
|
overflow: TextOverflow.visible,
|
||||||
fontSize: emojiSize,
|
fontSize: emojiSize,
|
||||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'
|
|||||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.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/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -104,6 +105,24 @@ extension ViewExtension on ViewPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FlowySvgData get iconData => layout.icon;
|
FlowySvgData get iconData => layout.icon;
|
||||||
|
|
||||||
|
Future<List<ViewPB>> getAncestors({bool includeSelf = false}) async {
|
||||||
|
final ancestors = <ViewPB>[];
|
||||||
|
if (includeSelf) {
|
||||||
|
ancestors.add(this);
|
||||||
|
}
|
||||||
|
var parent = await ViewBackendService.getView(parentViewId);
|
||||||
|
while (parent.isLeft()) {
|
||||||
|
// parent is not null
|
||||||
|
final view = parent.getLeftOrNull<ViewPB>();
|
||||||
|
if (view == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ancestors.add(view);
|
||||||
|
parent = await ViewBackendService.getView(view.parentViewId);
|
||||||
|
}
|
||||||
|
return ancestors.reversed.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewLayoutExtension on ViewLayoutPB {
|
extension ViewLayoutExtension on ViewLayoutPB {
|
||||||
|
@ -167,7 +167,8 @@ class InnerViewItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 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 && childViews.isNotEmpty) {
|
if (isExpanded) {
|
||||||
|
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('${categoryType.name} ${childView.id}'),
|
||||||
@ -191,6 +192,27 @@ 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
|
||||||
|
@ -3,11 +3,13 @@ import 'package:appflowy/workspace/application/view/view_service.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// TODO: Remove this file after the migration is done.
|
||||||
class ViewLeftBarItem extends StatefulWidget {
|
class ViewLeftBarItem extends StatefulWidget {
|
||||||
final ViewPB view;
|
ViewLeftBarItem({
|
||||||
|
required this.view,
|
||||||
|
}) : super(key: ValueKey(view.id));
|
||||||
|
|
||||||
ViewLeftBarItem({required this.view, Key? key})
|
final ViewPB view;
|
||||||
: super(key: ValueKey(view.hashCode));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ViewLeftBarItem> createState() => _ViewLeftBarItemState();
|
State<ViewLeftBarItem> createState() => _ViewLeftBarItemState();
|
||||||
|
@ -0,0 +1,236 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.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/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
// workspaces / ... / view_title
|
||||||
|
class ViewTitleBar extends StatefulWidget {
|
||||||
|
const ViewTitleBar({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ViewTitleBar> createState() => _ViewTitleBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewTitleBarState extends State<ViewTitleBar> {
|
||||||
|
late Future<List<ViewPB>> ancestors;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
ancestors = widget.view.getAncestors(
|
||||||
|
includeSelf: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ViewTitleBar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.view.id != widget.view.id) {
|
||||||
|
ancestors = widget.view.getAncestors(
|
||||||
|
includeSelf: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<ViewPB>>(
|
||||||
|
future: ancestors,
|
||||||
|
builder: ((context, snapshot) {
|
||||||
|
final ancestors = snapshot.data;
|
||||||
|
if (ancestors == null ||
|
||||||
|
snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
children: _buildViewTitles(ancestors),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildViewTitles(List<ViewPB> views) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
for (var i = 0; i < views.length; i++) {
|
||||||
|
final view = views[i];
|
||||||
|
children.add(
|
||||||
|
_ViewTitle(
|
||||||
|
view: view,
|
||||||
|
behavior: i == views.length - 1
|
||||||
|
? _ViewTitleBehavior.editable // only the last one is editable
|
||||||
|
: _ViewTitleBehavior.uneditable, // others are not editable
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (i != views.length - 1) {
|
||||||
|
// if not the last one, add a divider
|
||||||
|
children.add(const FlowyText.regular('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ViewTitleBehavior {
|
||||||
|
editable,
|
||||||
|
uneditable,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewTitle extends StatefulWidget {
|
||||||
|
const _ViewTitle({
|
||||||
|
required this.view,
|
||||||
|
this.behavior = _ViewTitleBehavior.editable,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final _ViewTitleBehavior behavior;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ViewTitle> createState() => _ViewTitleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewTitleState extends State<_ViewTitle> {
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
final textEditingController = TextEditingController();
|
||||||
|
late final viewListener = ViewListener(viewId: widget.view.id);
|
||||||
|
|
||||||
|
String name = '';
|
||||||
|
String icon = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
name = widget.view.name;
|
||||||
|
icon = widget.view.icon.value;
|
||||||
|
|
||||||
|
_resetTextEditingController();
|
||||||
|
viewListener.start(
|
||||||
|
onViewUpdated: (view) {
|
||||||
|
setState(() {
|
||||||
|
name = view.name;
|
||||||
|
icon = view.icon.value;
|
||||||
|
_resetTextEditingController();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
textEditingController.dispose();
|
||||||
|
popoverController.close();
|
||||||
|
viewListener.stop();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// root view
|
||||||
|
if (widget.view.parentViewId.isEmpty) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
FlowyText.regular(name),
|
||||||
|
const HSpace(4.0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final child = Row(
|
||||||
|
children: [
|
||||||
|
FlowyText.regular(
|
||||||
|
icon,
|
||||||
|
fontSize: 18.0,
|
||||||
|
),
|
||||||
|
const HSpace(2.0),
|
||||||
|
FlowyText.regular(name),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.behavior == _ViewTitleBehavior.uneditable) {
|
||||||
|
return FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
onTap: () {
|
||||||
|
context.read<TabsBloc>().openPlugin(widget.view);
|
||||||
|
},
|
||||||
|
text: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppFlowyPopover(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 300,
|
||||||
|
maxHeight: 44,
|
||||||
|
),
|
||||||
|
controller: popoverController,
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
offset: const Offset(0, 18),
|
||||||
|
popupBuilder: (context) {
|
||||||
|
// icon + textfield
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
EmojiPickerButton(
|
||||||
|
emoji: icon,
|
||||||
|
defaultIcon: widget.view.defaultIcon(),
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
offset: const Offset(0, 18),
|
||||||
|
onSubmitted: (emoji, _) {
|
||||||
|
ViewBackendService.updateViewIcon(
|
||||||
|
viewId: widget.view.id,
|
||||||
|
viewIcon: emoji,
|
||||||
|
);
|
||||||
|
popoverController.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
SizedBox(
|
||||||
|
height: 36.0,
|
||||||
|
width: 220,
|
||||||
|
child: FlowyTextField(
|
||||||
|
autoFocus: true,
|
||||||
|
controller: textEditingController,
|
||||||
|
onSubmitted: (text) {
|
||||||
|
if (text.isNotEmpty && text != name) {
|
||||||
|
ViewBackendService.updateView(
|
||||||
|
viewId: widget.view.id,
|
||||||
|
name: text,
|
||||||
|
);
|
||||||
|
popoverController.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
text: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetTextEditingController() {
|
||||||
|
textEditingController
|
||||||
|
..text = name
|
||||||
|
..selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: name.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -76,9 +76,11 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
|||||||
if (widget.autoFocus) {
|
if (widget.autoFocus) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
|
if (widget.controller == null) {
|
||||||
controller.selection = TextSelection.fromPosition(
|
controller.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: controller.text.length),
|
TextPosition(offset: controller.text.length),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user