fix: icon picker issues on mobile (#7113)

* fix: error displaying in Page style

* fix: error displaying in Favorite/Recent page

* fix: complete the filter logic of icon picker

* fix: the color picker showed when tapping down

* fix: icons are not supported in subpage blocks

* chore: add some tests

* fix: recent icons not working for grid header icon
This commit is contained in:
Morn 2025-01-03 10:04:14 +08:00 committed by GitHub
parent f7f99a162e
commit 15deb8ea79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 239 additions and 63 deletions

View File

@ -1,7 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.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:appflowy_editor/appflowy_editor.dart';
@ -11,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
// Test cases for the Document SubPageBlock that needs to be covered:
@ -37,7 +40,14 @@ import '../../shared/util.dart';
const _defaultPageName = "";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Document SubPageBlock tests', () {
testWidgets('Insert a new SubPageBlock from Slash menu items',
@ -498,6 +508,38 @@ void main() {
expect(find.text('Parent'), findsNWidgets(2));
});
testWidgets('Displaying icon of subpage', (tester) async {
const firstPage = 'FirstPage';
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: firstPage);
final icon = await tester.loadIcon();
/// create subpage
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_subPage_name.tr(),
offset: 100,
);
/// add icon
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.tapIcon(icon);
await tester.pumpAndSettle();
await tester.openPage(firstPage);
/// check if there is a icon in document
final iconWidget = find.byWidgetPredicate((w) {
if (w is! RawEmojiIconWidget) return false;
final iconData = w.emoji.emoji;
return iconData == icon.emoji;
});
expect(iconWidget, findsOneWidget);
});
});
}

View File

@ -1,9 +1,7 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
@ -27,21 +25,6 @@ void main() {
RecentIcons.enable = true;
});
Future<EmojiIconData> loadIcon() async {
await loadIconGroups();
final groups = kIconGroups!;
final firstGroup = groups.first;
final firstIcon = firstGroup.icons.first;
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.content,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
testWidgets('Update page emoji in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@ -160,7 +143,7 @@ void main() {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await loadIcon();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
@ -192,7 +175,7 @@ void main() {
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await loadIcon();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {

View File

@ -1,17 +1,30 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('document page style:', () {
double getCurrentEditorFontSize() {
@ -114,5 +127,37 @@ void main() {
);
expect(builtInCover, findsOneWidget);
});
testWidgets('page style icon', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton =
find.byKey(BottomNavigationBarItemType.add.valueKey);
await tester.tapButton(createPageButton);
/// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_layout_s);
/// select document plugins emoji
final pageStyleIcon = find.byType(PageStyleIcon);
/// there should be none of emoji
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
expect(noneText, findsOneWidget);
await tester.tapButton(pageStyleIcon);
/// select an emoji
const emoji = '😄';
await tester.tapEmoji(emoji);
await tester.tapSvgButton(FlowySvgs.m_layout_s);
expect(noneText, findsNothing);
expect(
find.descendant(
of: pageStyleIcon,
matching: find.text(emoji),
),
findsOneWidget,
);
});
});
}

View File

@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
}
}
Future<void> tapDown(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
PointerDeviceKind kind = PointerDeviceKind.touch,
bool pumpAndSettle = true,
int milliseconds = 500,
}) async {
final location = getCenter(finder);
final TestGesture gesture = await startGesture(
location,
pointer: pointer,
buttons: buttons,
kind: kind,
);
await gesture.cancel();
await gesture.down(location);
await gesture.cancel();
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 15),
);
}
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,

View File

@ -13,6 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
@ -23,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.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';
@ -898,6 +900,22 @@ extension CommonOperations on WidgetTester {
await tapAt(Offset.zero);
await pumpUntilNotFound(finder);
}
/// load icon list and return the first one
Future<EmojiIconData> loadIcon() async {
await loadIconGroups();
final groups = kIconGroups!;
final firstGroup = groups.first;
final firstIcon = firstGroup.icons.first;
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.content,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
}
extension SettingsFinder on CommonFinders {

View File

@ -42,6 +42,11 @@ extension EmojiTestExtension on WidgetTester {
),
);
expect(find.byType(IconColorPicker), findsNothing);
/// test for tapping down, it should not display the ColorPicker unless tapping up
await tapDown(selectedSvg);
expect(find.byType(IconColorPicker), findsNothing);
await tapButton(selectedSvg);
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);

View File

@ -178,15 +178,16 @@ class MobileViewPage extends StatelessWidget {
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
WidgetSpan(
child: SizedBox(
width: 20,
child: EmojiIconWidget(
emoji: icon,
emojiSize: 17.0,
if (icon.isNotEmpty)
WidgetSpan(
child: SizedBox(
width: 20,
child: EmojiIconWidget(
emoji: icon,
emojiSize: 17.0,
),
),
),
),
if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)),
TextSpan(
text: name,

View File

@ -48,12 +48,15 @@ class _PageStyleIconState extends State<PageStyleIcon> {
const HSpace(16.0),
FlowyText(LocaleKeys.document_plugins_emoji.tr()),
const Spacer(),
RawEmojiIconWidget(
emoji: icon.isNotEmpty
? icon
: EmojiIconData.emoji(LocaleKeys.pageStyle_none.tr()),
emojiSize: icon.isNotEmpty ? 22.0 : 16.0,
),
icon.isEmpty
? FlowyText(
LocaleKeys.pageStyle_none.tr(),
fontSize: 16.0,
)
: RawEmojiIconWidget(
emoji: icon,
emojiSize: 22.0,
),
const HSpace(6.0),
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
const HSpace(12.0),

View File

@ -1,8 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/plugins/trash/application/trash_listener.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -15,7 +17,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
@ -242,12 +243,9 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
children: [
const HSpace(10),
view.icon.value.isNotEmpty
? FlowyText.emoji(
view.icon.value,
fontSize: textStyle.fontSize,
lineHeight: textStyle.height,
color:
AFThemeExtension.of(context).strongText,
? RawEmojiIconWidget(
emoji: view.icon.toEmojiIconData(),
emojiSize: textStyle.fontSize ?? 16.0,
)
: view.defaultIcon(),
const HSpace(6),

View File

@ -39,8 +39,9 @@ class IconGroup {
final filteredIcons = icons
.where(
(icon) =>
icon.keywords.any((k) => k.contains(lowercaseKey)) ||
icon.name.contains(lowercaseKey),
icon.keywords
.any((k) => k.toLowerCase().contains(lowercaseKey)) ||
icon.name.toLowerCase().contains(lowercaseKey),
)
.toList();
return IconGroup(name: name, icons: filteredIcons);
@ -84,3 +85,23 @@ class Icon {
return '${iconGroup!.name}/$name';
}
}
class RecentIcon {
factory RecentIcon.fromJson(Map<String, dynamic> json) =>
RecentIcon(_$IconFromJson(json), json['groupName'] ?? '');
RecentIcon(this.icon, this.groupName);
final Icon icon;
final String groupName;
String get name => icon.name;
List<String> get keywords => icon.keywords;
String get content => icon.content;
Map<String, dynamic> toJson() => _$IconToJson(
Icon(name: name, keywords: keywords, content: content),
)..addAll({'groupName': groupName});
}

View File

@ -118,10 +118,13 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
iconGroups.add(
IconGroup(
name: _kRecentIconGroupName,
icons: recentIcons.sublist(
0,
min(recentIcons.length, widget.iconPerLine),
),
icons: recentIcons
.sublist(
0,
min(recentIcons.length, widget.iconPerLine),
)
.map((e) => e.icon)
.toList(),
),
);
}
@ -171,7 +174,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
color,
).toResult(isRandom: true),
);
RecentIcons.putIcon(value.$2);
RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name));
},
onKeywordChanged: (keyword) => {
debounce.call(() {
@ -303,30 +306,38 @@ class _IconPickerState extends State<IconPicker> {
icon: icon,
mutex: mutex,
onSelectedColor: (context, color) {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
iconGroup.name,
groupName,
icon.content,
icon.name,
color,
),
);
RecentIcons.putIcon(icon);
RecentIcons.putIcon(RecentIcon(icon, groupName));
PopoverContainer.of(context).close();
},
)
: _IconNoBackground(
icon: icon,
onSelectedIcon: () {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
iconGroup.name,
groupName,
icon.content,
icon.name,
null,
),
);
RecentIcons.putIcon(icon);
RecentIcons.putIcon(RecentIcon(icon, groupName));
},
);
},
@ -341,6 +352,16 @@ class _IconPickerState extends State<IconPicker> {
},
);
}
String getGroupName(int index) {
final recentIcons = RecentIcons.getIconsSync();
try {
return recentIcons[index].groupName;
} catch (e) {
Log.error('getGroupName with index: $index error', e);
return '';
}
}
}
class _IconNoBackground extends StatelessWidget {
@ -392,12 +413,20 @@ class _Icon extends StatefulWidget {
class _IconState extends State<_Icon> {
final PopoverController _popoverController = PopoverController();
@override
void dispose() {
super.dispose();
_popoverController.close();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 6),
mutex: widget.mutex,
clickHandler: PopoverClickHandler.gestureDetector,
child: _IconNoBackground(
icon: widget.icon,
onSelectedIcon: () => _popoverController.show(),

View File

@ -22,13 +22,10 @@ class RecentIcons {
await _put(FlowyIconType.emoji, id);
}
static Future<void> putIcon(Icon icon) async {
static Future<void> putIcon(RecentIcon icon) async {
await _put(
FlowyIconType.icon,
jsonEncode(
Icon(name: icon.name, keywords: icon.keywords, content: icon.content)
.toJson(),
),
jsonEncode(icon.toJson()),
);
}
@ -37,12 +34,18 @@ class RecentIcons {
return _dataMap[FlowyIconType.emoji.name] ?? [];
}
static Future<List<Icon>> getIcons() async {
static Future<List<RecentIcon>> getIcons() async {
await _load();
return getIconsSync();
}
static List<RecentIcon> getIconsSync() {
final iconList = _dataMap[FlowyIconType.icon.name] ?? [];
try {
return iconList
.map((e) => Icon.fromJson(jsonDecode(e) as Map<String, dynamic>))
.map(
(e) => RecentIcon.fromJson(jsonDecode(e) as Map<String, dynamic>),
)
.toList();
} catch (e) {
Log.error('RecentIcons getIcons with :$iconList', e);

View File

@ -46,16 +46,17 @@ void main() {
});
test('putIcons', () async {
List<Icon> icons = await RecentIcons.getIcons();
List<RecentIcon> icons = await RecentIcons.getIcons();
assert(icons.isEmpty);
await loadIconGroups();
final groups = kIconGroups!;
final List<Icon> localIcons = [];
final List<RecentIcon> localIcons = [];
for (final e in groups) {
localIcons.addAll(e.icons);
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
}
bool equalIcon(Icon a, Icon b) =>
bool equalIcon(RecentIcon a, RecentIcon b) =>
a.groupName == b.groupName &&
a.name == b.name &&
a.keywords.equals(b.keywords) &&
a.content == b.content;