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:
parent
42e7317cd4
commit
9586ea0e6f
@ -1,5 +1,6 @@
|
||||
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_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -155,7 +156,11 @@ void main() {
|
||||
const hand = '👋🏿';
|
||||
await tester.tapEmoji(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]);
|
||||
expect(
|
||||
tester.findFavoritePageName(names[1]),
|
||||
findsNWidgets(1),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
await tester.unfavoriteViewByName(gettingStarted);
|
||||
@ -99,7 +99,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
tester.findFavoritePageName(name),
|
||||
findsNothing,
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -127,11 +127,11 @@ void main() {
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
findsNWidgets(3),
|
||||
findsNWidgets(6),
|
||||
);
|
||||
|
||||
await tester.hoverOnPageName(
|
||||
@ -144,13 +144,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
findsNWidgets(2),
|
||||
tester.findAllFavoritePages(),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
|
||||
await tester.hoverOnPageName(
|
||||
@ -163,12 +158,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
),
|
||||
tester.findAllFavoritePages(),
|
||||
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 'sidebar_test.dart' as sidebar_test;
|
||||
import 'sidebar_expand_test.dart' as sidebar_expanded_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() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@ -11,4 +12,5 @@ void startTesting() {
|
||||
sidebar_test.main();
|
||||
sidebar_expanded_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/generated/flowy_svgs.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/startup/startup.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_more_action_button.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/protobuf/flowy-folder2/view.pb.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_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
import 'util.dart';
|
||||
|
||||
extension CommonOperations on WidgetTester {
|
||||
@ -442,6 +445,47 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
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 {
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
extension EmojiTestExtension on WidgetTester {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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/presentation/home/home_stack.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -164,7 +165,7 @@ extension Expectation on WidgetTester {
|
||||
}) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite &&
|
||||
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(
|
||||
String name, {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
@ -201,12 +211,23 @@ extension Expectation on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
void isPageWithIcon(String name, String emoji) {
|
||||
final pageName = findPageName(name);
|
||||
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||
final pageName = findPageName(
|
||||
name,
|
||||
layout: layout,
|
||||
);
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.text(emoji),
|
||||
);
|
||||
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/startup/plugin/plugin.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/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -190,7 +190,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
||||
Widget get leftBarItem => ViewTitleBar(view: notifier.view);
|
||||
|
||||
@override
|
||||
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/startup/plugin/plugin.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: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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@ -104,7 +104,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
}
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
Widget get leftBarItem => ViewTitleBar(view: view);
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||
|
@ -12,8 +12,11 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.onSubmitted,
|
||||
this.emojiPickerSize = const Size(300, 250),
|
||||
this.emojiPickerSize = const Size(360, 380),
|
||||
this.emojiSize = 18.0,
|
||||
this.defaultIcon,
|
||||
this.offset,
|
||||
this.direction,
|
||||
});
|
||||
|
||||
final String emoji;
|
||||
@ -21,6 +24,9 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
final Size emojiPickerSize;
|
||||
final void Function(String emoji, PopoverController? controller) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
final PopoverDirection? direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -32,6 +38,8 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
width: emojiPickerSize.width,
|
||||
height: emojiPickerSize.height,
|
||||
),
|
||||
offset: offset,
|
||||
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
||||
popupBuilder: (context) => Container(
|
||||
width: emojiPickerSize.width,
|
||||
height: emojiPickerSize.height,
|
||||
@ -41,7 +49,13 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
onExit: () {},
|
||||
),
|
||||
),
|
||||
child: FlowyTextButton(
|
||||
child: emoji.isEmpty && defaultIcon != null
|
||||
? FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: defaultIcon!,
|
||||
onTap: () => popoverController.show(),
|
||||
)
|
||||
: FlowyTextButton(
|
||||
emoji,
|
||||
overflow: TextOverflow.visible,
|
||||
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/document/document.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:flutter/material.dart';
|
||||
|
||||
@ -104,6 +105,24 @@ extension ViewExtension on ViewPB {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -167,7 +167,8 @@ class InnerViewItem extends StatelessWidget {
|
||||
);
|
||||
|
||||
// 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) {
|
||||
return ViewItem(
|
||||
key: ValueKey('${categoryType.name} ${childView.id}'),
|
||||
@ -191,6 +192,27 @@ class InnerViewItem extends StatelessWidget {
|
||||
...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
|
||||
|
@ -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:flutter/material.dart';
|
||||
|
||||
// TODO: Remove this file after the migration is done.
|
||||
class ViewLeftBarItem extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
ViewLeftBarItem({
|
||||
required this.view,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
|
||||
ViewLeftBarItem({required this.view, Key? key})
|
||||
: super(key: ValueKey(view.hashCode));
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
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) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
if (widget.controller == null) {
|
||||
controller.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: controller.text.length),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user