mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 06:37:14 -04:00
fix: issues related to the emoji icon picker (#7063)
* fix: remove the scrolling conflict of the icon picker on macOS * fix: the icon is not supported in sites tab * feat: keep the icon panel open after click ramdom * feat: the type of selector opened depends on the already set icon or emoji * feat: the skin tone of the random emoji follows the selected skin ton * fix: unit testing error
This commit is contained in:
parent
02eb0e0b83
commit
3836545682
25 changed files with 281 additions and 91 deletions
|
@ -1,4 +1,5 @@
|
|||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
|||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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';
|
||||
|
||||
|
@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
|||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.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';
|
||||
|
@ -12,7 +13,14 @@ import '../../shared/emoji.dart';
|
|||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Sidebar view item tests', () {
|
||||
testWidgets('Access view item context menu by right click', (tester) async {
|
||||
|
|
|
@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
|||
EmojiData? kCachedEmojiData;
|
||||
const _kRecentEmojiCategoryId = 'Recent';
|
||||
|
||||
class EmojiPickerResult {
|
||||
EmojiPickerResult({
|
||||
required this.emojiId,
|
||||
required this.emoji,
|
||||
this.isRandom = false,
|
||||
});
|
||||
|
||||
final String emojiId;
|
||||
final String emoji;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
class FlowyEmojiPicker extends StatefulWidget {
|
||||
const FlowyEmojiPicker({
|
||||
super.key,
|
||||
|
@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget {
|
|||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final EmojiSelectedCallback onEmojiSelected;
|
||||
final ValueChanged<EmojiPickerResult> onEmojiSelected;
|
||||
final int emojiPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
|
@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
|||
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
|
||||
),
|
||||
onEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(id, emoji);
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
|
@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
|||
onSkinToneChanged: (value) {
|
||||
skinTone.value = value;
|
||||
},
|
||||
onRandomEmojiSelected: widget.onEmojiSelected,
|
||||
onRandomEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
|||
const MobileEmojiPickerScreen({
|
||||
super.key,
|
||||
this.title,
|
||||
this.selectedType,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final PickerTabType? selectedType;
|
||||
final String? title;
|
||||
final List<PickerTabType> tabs;
|
||||
|
||||
static const routeName = '/emoji_picker';
|
||||
static const pageTitle = 'title';
|
||||
static const iconSelectedType = 'iconSelectedType';
|
||||
static const selectTabs = 'tabs';
|
||||
|
||||
@override
|
||||
|
@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
|||
body: SafeArea(
|
||||
child: FlowyIconEmojiPicker(
|
||||
tabs: tabs,
|
||||
initialType: selectedType,
|
||||
onSelectedEmoji: (r) {
|
||||
context.pop<EmojiIconData>(r);
|
||||
context.pop<EmojiIconData>(r.data);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State<RenameRowPopover> {
|
|||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 18),
|
||||
defaultIcon: const FlowySvg(FlowySvgs.document_s),
|
||||
onSubmitted: (emoji, _) {
|
||||
widget.onUpdateIcon(emoji);
|
||||
PopoverContainer.of(context).close();
|
||||
onSubmitted: (r, _) {
|
||||
widget.onUpdateIcon(r.data);
|
||||
if (!r.keepOpen) PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
const HSpace(6),
|
||||
|
|
|
@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget {
|
|||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
|
@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
|||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
|
@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
|||
height: emojiPickerSize.height,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: emoji.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
onSubmitted(r, popoverController);
|
||||
},
|
||||
|
@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
|||
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final String? title;
|
||||
final bool enable;
|
||||
final EdgeInsets? margin;
|
||||
|
@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
|||
final result = await context.push<EmojiIconData>(
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {MobileEmojiPickerScreen.pageTitle: title},
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle: title,
|
||||
MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
onSubmitted(result, null);
|
||||
onSubmitted(result.toSelectedResult(), null);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
|
|
@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
|||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
if (!r.keepOpen) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
|||
popupBuilder: (BuildContext popoverContext) {
|
||||
isPopoverOpen = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onIconOrCoverChanged(icon: result);
|
||||
_popoverController.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onIconOrCoverChanged(icon: r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -838,9 +838,7 @@ class _DocumentIconState extends State<DocumentIcon> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = EmojiIconWidget(
|
||||
emoji: widget.icon,
|
||||
);
|
||||
Widget child = EmojiIconWidget(emoji: widget.icon);
|
||||
|
||||
if (UniversalPlatform.isDesktopOrWeb) {
|
||||
child = AppFlowyPopover(
|
||||
|
@ -852,9 +850,10 @@ class _DocumentIconState extends State<DocumentIcon> {
|
|||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onChangeIcon(result);
|
||||
_popoverController.close();
|
||||
initialType: widget.icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onChangeIcon(r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -864,7 +863,12 @@ class _DocumentIconState extends State<DocumentIcon> {
|
|||
child: child,
|
||||
onTap: () async {
|
||||
final result = await context.push<EmojiIconData>(
|
||||
MobileEmojiPickerScreen.routeName,
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
widget.onChangeIcon(result);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
|
@ -67,24 +68,35 @@ class RawEmojiIconWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultEmoji = EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
final defaultEmoji = SizedBox(
|
||||
width: emojiSize,
|
||||
child: EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
try {
|
||||
switch (emoji.type) {
|
||||
case FlowyIconType.emoji:
|
||||
return EmojiText(
|
||||
emoji: emoji.emoji,
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
return SizedBox(
|
||||
width: emojiSize,
|
||||
child: EmojiText(
|
||||
emoji: emoji.emoji,
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
case FlowyIconType.icon:
|
||||
final iconData = IconsData.fromJson(jsonDecode(emoji.emoji));
|
||||
|
||||
/// Under the same width conditions, icons on macOS seem to appear
|
||||
/// larger than emojis, so 0.9 is used here to slightly reduce the
|
||||
/// size of the icons
|
||||
final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize;
|
||||
return IconWidget(
|
||||
data: iconData,
|
||||
size: emojiSize,
|
||||
size: iconSize,
|
||||
);
|
||||
default:
|
||||
return defaultEmoji;
|
||||
|
|
|
@ -35,7 +35,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
|||
builder: (context, state) {
|
||||
final icon = state.icon ?? EmojiIconData.none();
|
||||
return GestureDetector(
|
||||
onTap: () => _showIconSelector(context),
|
||||
onTap: () => _showIconSelector(context, icon),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 52,
|
||||
|
@ -66,7 +66,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
|||
);
|
||||
}
|
||||
|
||||
void _showIconSelector(BuildContext context) {
|
||||
void _showIconSelector(BuildContext context, EmojiIconData icon) {
|
||||
Navigator.pop(context);
|
||||
final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
|
||||
..add(const PageStyleIconEvent.initial());
|
||||
|
@ -85,11 +85,12 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
|||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
if (!r.keepOpen) Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget {
|
|||
|
||||
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
|||
),
|
||||
const HSpace(8.0),
|
||||
_RandomEmojiButton(
|
||||
skinTone: skinTone,
|
||||
emojiData: widget.emojiData,
|
||||
onRandomEmojiSelected: widget.onRandomEmojiSelected,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyEmojiSkinToneSelector(
|
||||
onEmojiSkinToneChanged: widget.onSkinToneChanged,
|
||||
onEmojiSkinToneChanged: (v) {
|
||||
setState(() {
|
||||
skinTone = v;
|
||||
});
|
||||
widget.onSkinToneChanged.call(v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
|||
|
||||
class _RandomEmojiButton extends StatelessWidget {
|
||||
const _RandomEmojiButton({
|
||||
required this.skinTone,
|
||||
required this.emojiData,
|
||||
required this.onRandomEmojiSelected,
|
||||
});
|
||||
|
||||
final EmojiSkinTone skinTone;
|
||||
final EmojiData emojiData;
|
||||
final EmojiSelectedCallback onRandomEmojiSelected;
|
||||
|
||||
|
@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget {
|
|||
),
|
||||
onTap: () {
|
||||
final random = emojiData.random;
|
||||
final emojiId = random.$1;
|
||||
final emoji = emojiData.getEmojiById(
|
||||
emojiId,
|
||||
skinTone: skinTone,
|
||||
);
|
||||
onRandomEmojiSelected(
|
||||
random.$1,
|
||||
random.$2,
|
||||
emojiId,
|
||||
emoji,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart' hide Icon;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
extension ToProto on FlowyIconType {
|
||||
|
@ -46,6 +47,10 @@ enum FlowyIconType {
|
|||
custom;
|
||||
}
|
||||
|
||||
extension FlowyIconTypeToPickerTabType on FlowyIconType {
|
||||
PickerTabType? toPickerTabType() => name.toPickerTabType();
|
||||
}
|
||||
|
||||
class EmojiIconData {
|
||||
factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');
|
||||
|
||||
|
@ -78,17 +83,35 @@ class EmojiIconData {
|
|||
bool get isNotEmpty => emoji.isNotEmpty;
|
||||
}
|
||||
|
||||
class SelectedEmojiIconResult {
|
||||
SelectedEmojiIconResult(this.data, this.keepOpen);
|
||||
|
||||
final EmojiIconData data;
|
||||
final bool keepOpen;
|
||||
|
||||
FlowyIconType get type => data.type;
|
||||
|
||||
String get emoji => data.emoji;
|
||||
}
|
||||
|
||||
extension EmojiIconDataToSelectedResultExtension on EmojiIconData {
|
||||
SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>
|
||||
SelectedEmojiIconResult(this, keepOpen);
|
||||
}
|
||||
|
||||
class FlowyIconEmojiPicker extends StatefulWidget {
|
||||
const FlowyIconEmojiPicker({
|
||||
super.key,
|
||||
this.onSelectedEmoji,
|
||||
this.initialType,
|
||||
this.enableBackgroundColorSelection = true,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final ValueChanged<EmojiIconData>? onSelectedEmoji;
|
||||
final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;
|
||||
final bool enableBackgroundColorSelection;
|
||||
final List<PickerTabType> tabs;
|
||||
final PickerTabType? initialType;
|
||||
|
||||
@override
|
||||
State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();
|
||||
|
@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget {
|
|||
|
||||
class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final controller = TabController(
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
late TabController controller;
|
||||
int currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialType = widget.initialType;
|
||||
if (initialType != null) {
|
||||
currentIndex = widget.tabs.indexOf(initialType);
|
||||
}
|
||||
controller = TabController(
|
||||
initialIndex: currentIndex,
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
|||
),
|
||||
_RemoveIconButton(
|
||||
onTap: () {
|
||||
widget.onSelectedEmoji?.call(EmojiIconData.none());
|
||||
widget.onSelectedEmoji
|
||||
?.call(EmojiIconData.none().toSelectedResult());
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
|||
return FlowyEmojiPicker(
|
||||
ensureFocus: true,
|
||||
emojiPerLine: _getEmojiPerLine(context),
|
||||
onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(emoji),
|
||||
),
|
||||
onEmojiSelected: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
|||
|
||||
Widget _buildIconPicker() {
|
||||
return FlowyIconPicker(
|
||||
ensureFocus: true,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: (result) {
|
||||
widget.onSelectedEmoji?.call(result.toEmojiIconData());
|
||||
onSelectedIcon: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -75,17 +75,31 @@ Future<List<IconGroup>> loadIconGroups() async {
|
|||
}
|
||||
}
|
||||
|
||||
class IconPickerResult {
|
||||
IconPickerResult(this.data, this.isRandom);
|
||||
|
||||
final IconsData data;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
extension IconsDataToIconPickerResultExtension on IconsData {
|
||||
IconPickerResult toResult({bool isRandom = false}) =>
|
||||
IconPickerResult(this, isRandom);
|
||||
}
|
||||
|
||||
class FlowyIconPicker extends StatefulWidget {
|
||||
const FlowyIconPicker({
|
||||
super.key,
|
||||
required this.onSelectedIcon,
|
||||
required this.enableBackgroundColorSelection,
|
||||
this.iconPerLine = 9,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final bool enableBackgroundColorSelection;
|
||||
final ValueChanged<IconsData> onSelectedIcon;
|
||||
final ValueChanged<IconPickerResult> onSelectedIcon;
|
||||
final int iconPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
|
||||
|
@ -142,6 +156,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: IconSearchBar(
|
||||
ensureFocus: widget.ensureFocus,
|
||||
onRandomTap: () {
|
||||
final value = kIconGroups?.randomIcon();
|
||||
if (value == null) {
|
||||
|
@ -154,8 +169,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
|||
value.$2.content,
|
||||
value.$2.name,
|
||||
color,
|
||||
),
|
||||
).toResult(isRandom: true),
|
||||
);
|
||||
RecentIcons.putIcon(value.$2);
|
||||
},
|
||||
onKeywordChanged: (keyword) => {
|
||||
debounce.call(() {
|
||||
|
@ -193,14 +209,14 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
|||
iconGroups: filteredIconGroups,
|
||||
enableBackgroundColorSelection:
|
||||
widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
}
|
||||
return IconPicker(
|
||||
iconGroups: iconGroups,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
},
|
||||
|
@ -278,6 +294,7 @@ class _IconPickerState extends State<IconPicker> {
|
|||
crossAxisCount: widget.iconPerLine,
|
||||
),
|
||||
itemCount: iconGroup.icons.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final icon = iconGroup.icons[index];
|
||||
|
|
|
@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget {
|
|||
super.key,
|
||||
required this.onRandomTap,
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final VoidCallback onRandomTap;
|
||||
final bool ensureFocus;
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
|
||||
@override
|
||||
|
@ -46,6 +48,7 @@ class _IconSearchBarState extends State<IconSearchBar> {
|
|||
Expanded(
|
||||
child: _SearchTextField(
|
||||
onKeywordChanged: widget.onKeywordChanged,
|
||||
ensureFocus: widget.ensureFocus,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
|
@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget {
|
|||
class _SearchTextField extends StatefulWidget {
|
||||
const _SearchTextField({
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<_SearchTextField> createState() => _SearchTextFieldState();
|
||||
|
@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
|||
final TextEditingController controller = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
|
|
@ -15,6 +15,16 @@ enum PickerTabType {
|
|||
}
|
||||
}
|
||||
|
||||
extension StringToPickerTabType on String {
|
||||
PickerTabType? toPickerTabType() {
|
||||
try {
|
||||
return PickerTabType.values.byName(this);
|
||||
} on ArgumentError {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PickerTab extends StatelessWidget {
|
||||
const PickerTab({
|
||||
super.key,
|
||||
|
|
|
@ -285,14 +285,21 @@ GoRoute _mobileEmojiPickerPageRoute() {
|
|||
state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];
|
||||
final selectTabs =
|
||||
state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';
|
||||
final selectedType = state
|
||||
.uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]
|
||||
?.toPickerTabType();
|
||||
final tabs = selectTabs
|
||||
.split('-')
|
||||
.map((e) => PickerTabType.values.byName(e))
|
||||
.toList();
|
||||
return MaterialExtendedPage(
|
||||
child: tabs.isEmpty
|
||||
? MobileEmojiPickerScreen(title: title)
|
||||
: MobileEmojiPickerScreen(title: title, tabs: tabs),
|
||||
? MobileEmojiPickerScreen(title: title, selectedType: selectedType)
|
||||
: MobileEmojiPickerScreen(
|
||||
title: title,
|
||||
selectedType: selectedType,
|
||||
tabs: tabs,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -96,9 +96,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||
margin: const EdgeInsets.all(0),
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
tabs: const [PickerTabType.emoji],
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onSelected(result);
|
||||
controller.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onSelected(r.data);
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
),
|
||||
child: MouseRegion(
|
||||
|
|
|
@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||
popupBuilder: (context) {
|
||||
isIconPickerOpened = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
initialType: iconData.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
controller.close();
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
||||
break;
|
||||
case ViewMoreActionType.changeIcon:
|
||||
if (data is! EmojiIconData) {
|
||||
if (data is! SelectedEmojiIconResult) {
|
||||
return;
|
||||
}
|
||||
final result = data;
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: data.data,
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.moveTo:
|
||||
|
|
|
@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget {
|
|||
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||
onEditing(false);
|
||||
onAction(e, data);
|
||||
controller.close();
|
||||
bool enableClose = true;
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.keepOpen) enableClose = false;
|
||||
}
|
||||
if (enableClose) controller.close();
|
||||
}),
|
||||
)
|
||||
.toList();
|
||||
|
@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
|||
margin: const EdgeInsets.all(0),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),
|
||||
onSelectedEmoji: (result) => onTap(controller, result),
|
||||
),
|
||||
child: child,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
|
@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
final icon = publishInfoView.view.icon.value;
|
||||
final icon = publishInfoView.view.icon.toEmojiIconData();
|
||||
return icon.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 18.0,
|
||||
)
|
||||
? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0)
|
||||
: publishInfoView.view.defaultIcon();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
getName: LocaleKeys.document_plugins_emoji.tr,
|
||||
|
@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyEmojiPicker(
|
||||
onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji),
|
||||
onEmojiSelected: (r) => widget.onSubmitted(r.emoji),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
|
|||
}
|
||||
|
||||
Future<void> _updateViewIcon(
|
||||
EmojiIconData emoji,
|
||||
SelectedEmojiIconResult r,
|
||||
PopoverController? _,
|
||||
) async {
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.viewId,
|
||||
viewIcon: emoji,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
widget.popoverController.close();
|
||||
if (!r.keepOpen) {
|
||||
widget.popoverController.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -757,8 +757,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "8a9fa49"
|
||||
resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028"
|
||||
ref: "355aa56"
|
||||
resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8"
|
||||
url: "https://github.com/LucasXu0/emoji_mart.git"
|
||||
source: git
|
||||
version: "1.0.2"
|
||||
|
|
|
@ -73,7 +73,7 @@ dependencies:
|
|||
flutter_emoji_mart:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/emoji_mart.git
|
||||
ref: "8a9fa49"
|
||||
ref: "355aa56"
|
||||
flutter_math_fork: ^0.7.3
|
||||
flutter_slidable: ^3.0.0
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue