feat: support customizing page icon (#3849)

* chore: don't use cache when building release package

* feat: refactor icon widget design

* feat: sync the emoji between page and view

* feat: use cache to store the emoji data to prevent reloading

* feat: customize the emoji item builder

* feat: add i18n and shuffle emoji button

* fix: integration test

* feat: replace emoji picker in Grid and slash menu

* feat: support adding icon on mobile platform

* feat: support adding and removing icon on mobile

* test: add integration tests
This commit is contained in:
Lucas.Xu 2023-11-02 15:24:17 +08:00 committed by GitHub
parent 21d34d1fe0
commit c34a7a92fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1116 additions and 256 deletions

View File

@ -59,7 +59,6 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -70,11 +69,6 @@ jobs:
components: rustfmt components: rustfmt
profile: minimal profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites - name: Install prerequisites
working-directory: frontend working-directory: frontend
run: | run: |
@ -151,7 +145,6 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -162,11 +155,6 @@ jobs:
components: rustfmt components: rustfmt
profile: minimal profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites - name: Install prerequisites
working-directory: frontend working-directory: frontend
run: | run: |
@ -257,7 +245,6 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@ -266,11 +253,6 @@ jobs:
targets: ${{ matrix.job.targets }} targets: ${{ matrix.job.targets }}
components: rustfmt components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites - name: Install prerequisites
working-directory: frontend working-directory: frontend
run: | run: |
@ -366,7 +348,6 @@ jobs:
with: with:
channel: "stable" channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -377,11 +358,6 @@ jobs:
components: rustfmt components: rustfmt
profile: minimal profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites - name: Install prerequisites
working-directory: frontend working-directory: frontend
run: | run: |

View File

@ -42,7 +42,6 @@ void main() {
await tester.hoverRowBanner(); await tester.hoverRowBanner();
await tester.openEmojiPicker(); await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
// After select the emoji, the EmojiButton will show up // After select the emoji, the EmojiButton will show up
@ -60,12 +59,10 @@ void main() {
await tester.openFirstRowDetailPage(); await tester.openFirstRowDetailPage();
await tester.hoverRowBanner(); await tester.hoverRowBanner();
await tester.openEmojiPicker(); await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
// Update existing selected emoji // Update existing selected emoji
await tester.tapButton(find.byType(EmojiButton)); await tester.tapButton(find.byType(EmojiButton));
await tester.switchToEmojiList();
await tester.tapEmoji('😅'); await tester.tapEmoji('😅');
// The emoji already displayed in the row banner // The emoji already displayed in the row banner
@ -89,7 +86,6 @@ void main() {
await tester.openFirstRowDetailPage(); await tester.openFirstRowDetailPage();
await tester.hoverRowBanner(); await tester.hoverRowBanner();
await tester.openEmojiPicker(); await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
// Remove the emoji // Remove the emoji

View File

@ -1,4 +1,8 @@
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:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -61,7 +65,6 @@ void main() {
// Insert a document icon // Insert a document icon
await tester.editor.tapAddIconButton(); await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀'); tester.expectToSeeDocumentIcon('😀');
@ -73,13 +76,11 @@ void main() {
// Add the icon back for further testing // Add the icon back for further testing
await tester.editor.hoverOnCoverToolbar(); await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton(); await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀'); tester.expectToSeeDocumentIcon('😀');
// Change the document icon // Change the document icon
await tester.editor.tapOnIconWidget(); await tester.editor.tapOnIconWidget();
await tester.switchToEmojiList();
await tester.tapEmoji('😅'); await tester.tapEmoji('😅');
tester.expectToSeeDocumentIcon('😅'); tester.expectToSeeDocumentIcon('😅');
@ -102,7 +103,6 @@ void main() {
// Insert a document icon // Insert a document icon
await tester.editor.tapAddIconButton(); await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀'); await tester.tapEmoji('😀');
// Insert a document cover // Insert a document cover
@ -116,5 +116,46 @@ void main() {
await tester.editor.hoverOnCoverToolbar(); await tester.editor.hoverOnCoverToolbar();
tester.expectToSeeEmptyDocumentHeaderToolbar(); tester.expectToSeeEmptyDocumentHeaderToolbar();
}); });
testWidgets('shuffle icon', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
// click the shuffle button
await tester.tapButton(
find.byTooltip(LocaleKeys.emoji_random.tr()),
);
tester.expectDocumentIconNotNull();
});
testWidgets('change skin tone', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
final searchEmojiTextField = find.byWidgetPredicate(
(widget) =>
widget is TextField &&
widget.decoration!.hintText == LocaleKeys.emoji_search.tr(),
);
await tester.enterText(
searchEmojiTextField,
'hand',
);
// change skin tone
await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark);
// select an icon with skin tone
const hand = '👋🏿';
await tester.tapEmoji(hand);
tester.expectToSeeDocumentIcon(hand);
tester.isPageWithIcon(gettingStarted, hand);
});
}); });
} }

View File

@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -81,9 +83,15 @@ extension AppFlowyTestBase on WidgetTester {
} }
Future<void> waitUntilSignInPageShow() async { Future<void> waitUntilSignInPageShow() async {
final finder = find.byType(GoButton); if (isCloudEnabled) {
await pumpUntilFound(finder); final finder = find.byType(SignInAnonymousButton);
expect(finder, findsOneWidget); await pumpUntilFound(finder);
expect(finder, findsOneWidget);
} else {
final finder = find.byType(GoButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
}
} }
Future<void> pumpUntilFound( Future<void> pumpUntilFound(

View File

@ -7,6 +7,7 @@ import 'package:appflowy/generated/locale_keys.g.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';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
@ -27,8 +28,15 @@ import 'util.dart';
extension CommonOperations on WidgetTester { extension CommonOperations on WidgetTester {
/// Tap the GetStart button on the launch page. /// Tap the GetStart button on the launch page.
Future<void> tapGoButton() async { Future<void> tapGoButton() async {
// local version
final goButton = find.byType(GoButton); final goButton = find.byType(GoButton);
await tapButton(goButton); if (goButton.evaluate().isNotEmpty) {
await tapButton(goButton);
} else {
// cloud version
final anonymousButton = find.byType(SignInAnonymousButton);
await tapButton(anonymousButton);
}
if (Platform.isWindows) { if (Platform.isWindows) {
await pumpAndSettle(const Duration(milliseconds: 200)); await pumpAndSettle(const Duration(milliseconds: 200));

View File

@ -38,7 +38,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
@ -53,7 +52,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
@ -618,7 +617,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> openEmojiPicker() async { Future<void> openEmojiPicker() async {
await tapButton(find.byType(EmojiPickerButton)); await tapButton(find.byType(EmojiPickerButton));
await tapButton(find.byType(EmojiSelectionMenu));
} }
Future<void> tapDateCellInRowDetailPage() async { Future<void> tapDateCellInRowDetailPage() async {

View File

@ -1,15 +1,18 @@
import 'dart:ui'; import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.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/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart'; 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';
@ -54,7 +57,18 @@ class EditorOperations {
await tester.tapButtonWithName( await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addIcon.tr(), LocaleKeys.document_plugins_cover_addIcon.tr(),
); );
expect(find.byType(EmojiPopover), findsOneWidget); expect(find.byType(FlowyEmojiPicker), findsOneWidget);
}
/// Taps on the 'Skin tone' button
///
/// Must call [tapAddIconButton] first.
Future<void> changeEmojiSkinTone(EmojiSkinTone skinTone) async {
await tester.tapButton(
find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()),
);
final skinToneButton = find.text(EmojiSkinToneWrapper(skinTone).name);
await tester.tapButton(skinToneButton);
} }
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
@ -62,7 +76,10 @@ class EditorOperations {
Finder button = Finder button =
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
if (isInPicker) { if (isInPicker) {
button = find.descendant(of: find.byType(EmojiPopover), matching: button); button = find.descendant(
of: find.byType(FlowyIconPicker),
matching: button,
);
} }
await tester.tapButton(button); await tester.tapButton(button);

View File

@ -1,15 +1,8 @@
import 'package:flutter/material.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 {
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async { Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji); final emojiWidget = find.text(emoji);
await tapButton(emojiWidget); await tapButton(emojiWidget);

View File

@ -108,6 +108,13 @@ extension Expectation on WidgetTester {
expect(iconWidget, findsOneWidget); expect(iconWidget, findsOneWidget);
} }
void expectDocumentIconNotNull() {
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty,
);
expect(iconWidget, findsOneWidget);
}
void expectToSeeDocumentCover(CoverType type) { void expectToSeeDocumentCover(CoverType type) {
final findCover = find.byWidgetPredicate( final findCover = find.byWidgetPredicate(
(widget) => widget is DocumentCover && widget.coverType == type, (widget) => widget is DocumentCover && widget.coverType == type,
@ -193,4 +200,13 @@ extension Expectation on WidgetTester {
matching: findPageName(name, layout: layout), matching: findPageName(name, layout: layout),
); );
} }
void isPageWithIcon(String name, String emoji) {
final pageName = findPageName(name);
final icon = find.descendant(
of: pageName,
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
}
} }

View File

@ -106,12 +106,22 @@ class _MobileViewPageState extends State<MobileViewPage> {
} }
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) { Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
final icon = view?.icon.value;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
titleSpacing: 0, titleSpacing: 0,
title: FlowyText.semibold( title: Row(
view?.name ?? widget.title ?? '', children: [
fontSize: 14.0, if (icon != null)
FlowyText(
'$icon ',
fontSize: 22.0,
),
FlowyText.regular(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
),
],
), ),
leading: AppBarBackButton( leading: AppBarBackButton(
onTap: () => context.pop(), onTap: () => context.pop(),

View File

@ -303,11 +303,8 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
_buildLeftIcon(), _buildLeftIcon(),
const HSpace(4), const HSpace(4),
// icon // icon
SizedBox.square( _buildViewIconButton(),
dimension: 22, const HSpace(8),
child: widget.view.defaultIcon(),
),
const HSpace(12),
// title // title
Expanded( Expanded(
child: FlowyText.regular( child: FlowyText.regular(
@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
return child; return child;
} }
Widget _buildViewIconButton() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText(
widget.view.icon.value,
fontSize: 24.0,
)
: SizedBox.square(
dimension: 26.0,
child: widget.view.defaultIcon(),
);
return icon;
}
// > button or · button // > button or · button
// show > if the view is expandable. // show > if the view is expandable.
// show · if the view can't contain child views. // show · if the view can't contain child views.

View File

@ -0,0 +1,95 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// use a global value to store the selected emoji to prevent reloading every time.
EmojiData? _cachedEmojiData;
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
super.key,
required this.onEmojiSelected,
});
final EmojiSelectedCallback onEmojiSelected;
@override
State<FlowyEmojiPicker> createState() => _FlowyEmojiPickerState();
}
class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
EmojiData? emojiData;
@override
void initState() {
super.initState();
// load the emoji data from cache if it's available
if (_cachedEmojiData != null) {
emojiData = _cachedEmojiData;
} else {
EmojiData.builtIn().then(
(value) {
_cachedEmojiData = value;
setState(() {
emojiData = value;
});
},
);
}
}
@override
Widget build(BuildContext context) {
if (emojiData == null) {
return const Center(
child: SizedBox.square(
dimension: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
);
}
return EmojiPicker(
emojiData: emojiData!,
configuration: EmojiPickerConfiguration(
showSectionHeader: true,
showTabs: false,
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
),
onEmojiSelected: widget.onEmojiSelected,
headerBuilder: (context, category) {
return FlowyEmojiHeader(
category: category,
);
},
itemBuilder: (context, emojiId, emoji, callback) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(2.0),
icon: FlowyText(
emoji,
fontSize: 28.0,
),
onPressed: () => callback(emojiId, emoji),
);
},
searchBarBuilder: (context, keyword, skinTone) {
return FlowyEmojiSearchBar(
emojiData: emojiData!,
onKeywordChanged: (value) {
keyword.value = value;
},
onSkinToneChanged: (value) {
skinTone.value = value;
},
onRandomEmojiSelected: widget.onEmojiSelected,
);
},
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FlowyEmojiHeader extends StatelessWidget {
const FlowyEmojiHeader({
super.key,
required this.category,
});
final Category category;
@override
Widget build(BuildContext context) {
if (PlatformExtension.isDesktopOrWeb) {
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
color: Theme.of(context).cardColor,
child: FlowyText.regular(category.id),
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 40,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
color: Theme.of(context).cardColor,
child: Padding(
padding: const EdgeInsets.only(
top: 14.0,
bottom: 4.0,
),
child: FlowyText.regular(category.id),
),
),
const Divider(
height: 1,
thickness: 1,
),
],
);
}
}
}

View File

@ -0,0 +1,41 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
class FlowyEmojiPickerI18n extends EmojiPickerI18n {
@override
String get activity => LocaleKeys.emoji_categories_activities.tr();
@override
String get flags => LocaleKeys.emoji_categories_flags.tr();
@override
String get foods => LocaleKeys.emoji_categories_food.tr();
@override
String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr();
@override
String get nature => LocaleKeys.emoji_categories_nature.tr();
@override
String get objects => LocaleKeys.emoji_categories_objects.tr();
@override
String get people => LocaleKeys.emoji_categories_smileys.tr();
@override
String get places => LocaleKeys.emoji_categories_places.tr();
@override
String get search => LocaleKeys.emoji_search.tr();
@override
String get symbols => LocaleKeys.emoji_categories_symbols.tr();
@override
String get searchHintText => LocaleKeys.emoji_search.tr();
@override
String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr();
}

View File

@ -0,0 +1,22 @@
import 'package:appflowy/plugins/base/icon/icon_picker_page.dart';
import 'package:flutter/material.dart';
class MobileEmojiPickerScreen extends StatelessWidget {
static const routeName = '/emoji_picker';
static const viewId = 'id';
const MobileEmojiPickerScreen({
super.key,
required this.id,
});
/// view id
final String id;
@override
Widget build(BuildContext context) {
return IconPickerPage(
id: id,
);
}
}

View File

@ -0,0 +1,156 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
typedef EmojiKeywordChangedCallback = void Function(String keyword);
typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);
class FlowyEmojiSearchBar extends StatefulWidget {
const FlowyEmojiSearchBar({
super.key,
required this.emojiData,
required this.onKeywordChanged,
required this.onSkinToneChanged,
required this.onRandomEmojiSelected,
});
final EmojiData emojiData;
final EmojiKeywordChangedCallback onKeywordChanged;
final EmojiSkinToneChanged onSkinToneChanged;
final EmojiSelectedCallback onRandomEmojiSelected;
@override
State<FlowyEmojiSearchBar> createState() => _FlowyEmojiSearchBarState();
}
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
final TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0,
),
child: Row(
children: [
Expanded(
child: _SearchTextField(
onKeywordChanged: widget.onKeywordChanged,
),
),
const HSpace(6.0),
_RandomEmojiButton(
emojiData: widget.emojiData,
onRandomEmojiSelected: widget.onRandomEmojiSelected,
),
const HSpace(6.0),
FlowyEmojiSkinToneSelector(
onEmojiSkinToneChanged: widget.onSkinToneChanged,
),
const HSpace(6.0),
],
),
);
}
}
class _RandomEmojiButton extends StatelessWidget {
const _RandomEmojiButton({
required this.emojiData,
required this.onRandomEmojiSelected,
});
final EmojiData emojiData;
final EmojiSelectedCallback onRandomEmojiSelected;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.emoji_random.tr(),
child: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.shuffle_rounded,
),
onTap: () {
final random = emojiData.random;
onRandomEmojiSelected(
random.$1,
random.$2,
);
},
),
);
}
}
class _SearchTextField extends StatefulWidget {
const _SearchTextField({
required this.onKeywordChanged,
});
final EmojiKeywordChangedCallback onKeywordChanged;
@override
State<_SearchTextField> createState() => _SearchTextFieldState();
}
class _SearchTextFieldState extends State<_SearchTextField> {
final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 32.0,
),
child: FlowyTextField(
autoFocus: true,
hintText: LocaleKeys.emoji_search.tr(),
controller: controller,
onChanged: widget.onKeywordChanged,
prefixIcon: const Padding(
padding: EdgeInsets.only(
left: 8.0,
right: 4.0,
),
child: FlowySvg(
FlowySvgs.search_s,
),
),
prefixIconConstraints: const BoxConstraints(
maxHeight: 18.0,
),
suffixIcon: Padding(
padding: const EdgeInsets.all(4.0),
child: FlowyButton(
text: const FlowySvg(
FlowySvgs.close_lg,
),
margin: EdgeInsets.zero,
useIntrinsicWidth: true,
onTap: () {
controller.clear();
widget.onKeywordChanged('');
},
),
),
),
);
}
}

View File

@ -0,0 +1,112 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
// use a temporary global value to store last selected skin tone
EmojiSkinTone? lastSelectedEmojiSkinTone;
class FlowyEmojiSkinToneSelector extends StatefulWidget {
const FlowyEmojiSkinToneSelector({
super.key,
required this.onEmojiSkinToneChanged,
});
final EmojiSkinToneChanged onEmojiSkinToneChanged;
@override
State<FlowyEmojiSkinToneSelector> createState() =>
_FlowyEmojiSkinToneSelectorState();
}
class _FlowyEmojiSkinToneSelectorState
extends State<FlowyEmojiSkinToneSelector> {
EmojiSkinTone skinTone = EmojiSkinTone.none;
@override
Widget build(BuildContext context) {
return PopoverActionList<EmojiSkinToneWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8),
actions: EmojiSkinTone.values
.map((action) => EmojiSkinToneWrapper(action))
.toList(),
buildChild: (controller) {
return FlowyTooltip(
message: LocaleKeys.emoji_selectSkinTone.tr(),
child: FlowyIconButton(
icon: Padding(
// add a left padding to align the emoji center
padding: const EdgeInsets.only(
left: 3.0,
),
child: FlowyText(
lastSelectedEmojiSkinTone?.icon ?? '',
fontSize: 22.0,
),
),
onPressed: () => controller.show(),
),
);
},
onSelected: (action, controller) async {
widget.onEmojiSkinToneChanged(action.inner);
setState(() {
lastSelectedEmojiSkinTone = action.inner;
});
controller.close();
},
);
}
}
class EmojiSkinToneWrapper extends ActionCell {
EmojiSkinToneWrapper(this.inner);
final EmojiSkinTone inner;
Widget? icon(Color iconColor) => null;
@override
String get name {
final String i18n;
switch (inner) {
case EmojiSkinTone.none:
i18n = LocaleKeys.emoji_skinTone_default.tr();
case EmojiSkinTone.light:
i18n = LocaleKeys.emoji_skinTone_light.tr();
case EmojiSkinTone.mediumLight:
i18n = LocaleKeys.emoji_skinTone_mediumLight.tr();
case EmojiSkinTone.medium:
i18n = LocaleKeys.emoji_skinTone_medium.tr();
case EmojiSkinTone.mediumDark:
i18n = LocaleKeys.emoji_skinTone_mediumDark.tr();
case EmojiSkinTone.dark:
i18n = LocaleKeys.emoji_skinTone_dark.tr();
}
return '${inner.icon} $i18n';
}
}
extension on EmojiSkinTone {
String get icon {
switch (this) {
case EmojiSkinTone.none:
return '';
case EmojiSkinTone.light:
return '✋🏻';
case EmojiSkinTone.mediumLight:
return '✋🏼';
case EmojiSkinTone.medium:
return '✋🏽';
case EmojiSkinTone.mediumDark:
return '✋🏾';
case EmojiSkinTone.dark:
return '✋🏿';
}
}
}

View File

@ -0,0 +1,121 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
enum FlowyIconType {
emoji,
icon,
custom;
}
class FlowyIconPicker extends StatefulWidget {
const FlowyIconPicker({
super.key,
required this.onSelected,
});
final void Function(FlowyIconType type, String value) onSelected;
@override
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
}
class _FlowyIconPickerState extends State<FlowyIconPicker>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
// ONLY supports emoji picker for now
return DefaultTabController(
length: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
_buildTabs(context),
const Spacer(),
_RemoveIconButton(
onTap: () {
widget.onSelected(FlowyIconType.icon, '');
},
),
],
),
const Divider(
height: 2,
),
Expanded(
child: TabBarView(
children: [
FlowyEmojiPicker(
onEmojiSelected: (_, emoji) {
widget.onSelected(FlowyIconType.emoji, emoji);
},
),
],
),
),
],
),
);
}
Widget _buildTabs(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
overlayColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.secondary,
),
padding: EdgeInsets.zero,
tabs: [
FlowyHover(
style: const HoverStyle(borderRadius: BorderRadius.zero),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
child: FlowyText(
LocaleKeys.emoji_emojiTab.tr(),
),
),
)
],
),
);
}
}
class _RemoveIconButton extends StatelessWidget {
const _RemoveIconButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 28,
child: FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class IconPickerPage extends StatefulWidget {
const IconPickerPage({
super.key,
required this.id,
});
/// view id
final String id;
@override
State<IconPickerPage> createState() => _IconPickerPageState();
}
class _IconPickerPageState extends State<IconPickerPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: const FlowyText.semibold(
'Page icon',
fontSize: 14.0,
),
leading: AppBarBackButton(
onTap: () => context.pop(),
),
),
body: SafeArea(
child: FlowyIconPicker(
onSelected: (_, emoji) {
ViewBackendService.updateViewIcon(
viewId: widget.id,
viewIcon: emoji,
);
context.pop();
},
),
),
);
}
}

View File

@ -186,10 +186,9 @@ class _BannerTitleState extends State<_BannerTitle> {
controller: widget.popoverController, controller: widget.popoverController,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300),
popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) { popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
context context.read<RowBannerBloc>().add(RowBannerEvent.setIcon(emoji));
.read<RowBannerBloc>()
.add(RowBannerEvent.setIcon(emoji.emoji));
widget.popoverController.close(); widget.popoverController.close();
}), }),
child: Row(children: children), child: Row(children: children),
@ -199,7 +198,7 @@ class _BannerTitleState extends State<_BannerTitle> {
} }
} }
typedef OnSubmittedEmoji = void Function(Emoji emoji); typedef OnSubmittedEmoji = void Function(String emoji);
const _kBannerActionHeight = 40.0; const _kBannerActionHeight = 40.0;
class EmojiButton extends StatelessWidget { class EmojiButton extends StatelessWidget {
@ -286,12 +285,9 @@ class RemoveEmojiButton extends StatelessWidget {
} }
Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) { Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
return SizedBox( return EmojiSelectionMenu(
height: 250, onSubmitted: onSubmitted,
child: EmojiSelectionMenu( onExit: () {},
onSubmitted: onSubmitted,
onExit: () {},
),
); );
} }

View File

@ -12,6 +12,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/base64_string.dart'; import 'package:appflowy/util/base64_string.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
@ -111,9 +112,7 @@ class _DocumentPageState extends State<DocumentPage> {
styleCustomizer: EditorStyleCustomizer( styleCustomizer: EditorStyleCustomizer(
context: context, context: context,
// the 44 is the width of the left action list // the 44 is the width of the left action list
padding: PlatformExtension.isMobile padding: EditorStyleCustomizer.documentPadding,
? const EdgeInsets.only(left: 20, right: 20)
: const EdgeInsets.only(left: 40, right: 40 + 44),
), ),
header: _buildCoverAndIcon(context), header: _buildCoverAndIcon(context),
); );
@ -140,6 +139,13 @@ class _DocumentPageState extends State<DocumentPage> {
return DocumentHeaderNodeWidget( return DocumentHeaderNodeWidget(
node: page, node: page,
editorState: editorState!, editorState: editorState!,
view: widget.view,
onIconChanged: (icon) async {
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: icon,
);
},
); );
} }

View File

@ -15,7 +15,7 @@ class EmojiPickerButton extends StatelessWidget {
final String emoji; final String emoji;
final double emojiSize; final double emojiSize;
final Size emojiPickerSize; final Size emojiPickerSize;
final void Function(Emoji emoji, PopoverController controller) onSubmitted; final void Function(String emoji, PopoverController controller) onSubmitted;
final PopoverController popoverController = PopoverController(); final PopoverController popoverController = PopoverController();
@override @override

View File

@ -186,7 +186,7 @@ class _CalloutBlockComponentWidgetState
), // force to refresh the popover state ), // force to refresh the popover state
emoji: emoji, emoji: emoji,
onSubmitted: (emoji, controller) { onSubmitted: (emoji, controller) {
setEmoji(emoji.emoji); setEmoji(emoji);
controller.close(); controller.close();
}, },
), ),

View File

@ -2,17 +2,23 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'cover_editor.dart'; import 'cover_editor.dart';
import 'emoji_icon_widget.dart';
import 'emoji_popover.dart';
const double kCoverHeight = 250.0; const double kCoverHeight = 250.0;
const double kIconHeight = 60.0; const double kIconHeight = 60.0;
@ -45,13 +51,17 @@ enum CoverType {
class DocumentHeaderNodeWidget extends StatefulWidget { class DocumentHeaderNodeWidget extends StatefulWidget {
const DocumentHeaderNodeWidget({ const DocumentHeaderNodeWidget({
super.key,
required this.node, required this.node,
required this.editorState, required this.editorState,
super.key, required this.onIconChanged,
required this.view,
}); });
final Node node; final Node node;
final EditorState editorState; final EditorState editorState;
final void Function(String icon) onIconChanged;
final ViewPB view;
@override @override
State<DocumentHeaderNodeWidget> createState() => State<DocumentHeaderNodeWidget> createState() =>
@ -64,19 +74,33 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
); );
String? get coverDetails => String? get coverDetails =>
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];
String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];
bool get hasIcon => bool get hasIcon => viewIcon.isNotEmpty;
widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false;
bool get hasCover => coverType != CoverType.none; bool get hasCover => coverType != CoverType.none;
String viewIcon = '';
late final ViewListener viewListener;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final value = widget.view.icon.value;
viewIcon = value.isNotEmpty ? value : icon ?? '';
widget.node.addListener(_reload); widget.node.addListener(_reload);
viewListener = ViewListener(
viewId: widget.view.id,
)..start(
onViewUpdated: (p0) {
setState(() {
viewIcon = p0.icon.value;
});
},
);
} }
@override @override
void dispose() { void dispose() {
viewListener.stop();
widget.node.removeListener(_reload); widget.node.removeListener(_reload);
super.dispose(); super.dispose();
} }
@ -108,7 +132,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
), ),
if (hasIcon) if (hasIcon)
Positioned( Positioned(
left: 80, left: PlatformExtension.isDesktopOrWeb ? 80 : 20,
// if hasCover, there shouldn't be icons present so the icon can // if hasCover, there shouldn't be icons present so the icon can
// be closer to the bottom. // be closer to the bottom.
bottom: bottom:
@ -116,8 +140,10 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
child: DocumentIcon( child: DocumentIcon(
editorState: widget.editorState, editorState: widget.editorState,
node: widget.node, node: widget.node,
icon: icon, icon: viewIcon,
onIconChanged: (icon) => _saveCover(icon: icon), onIconChanged: (icon) async {
_saveCover(icon: icon);
},
), ),
), ),
], ],
@ -153,6 +179,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
} }
if (icon != null) { if (icon != null) {
attributes[DocumentHeaderBlockKeys.icon] = icon; attributes[DocumentHeaderBlockKeys.icon] = icon;
widget.onIconChanged(icon);
} }
transaction.updateNode(widget.node, attributes); transaction.updateNode(widget.node, attributes);
@ -188,29 +215,42 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
final PopoverController _popoverController = PopoverController(); final PopoverController _popoverController = PopoverController();
@override
void initState() {
super.initState();
isHidden = PlatformExtension.isDesktopOrWeb;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( Widget child = Container(
onEnter: (event) => setHidden(false), alignment: Alignment.bottomLeft,
onExit: (event) { width: double.infinity,
if (!isPopoverOpen) { padding: EditorStyleCustomizer.documentPadding,
setHidden(true); child: SizedBox(
} height: 28,
}, child: Row(
opaque: false, crossAxisAlignment: CrossAxisAlignment.stretch,
child: Container( children: buildRowChildren(),
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 40),
child: SizedBox(
height: 28,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: buildRowChildren(),
),
), ),
), ),
); );
if (PlatformExtension.isDesktopOrWeb) {
child = MouseRegion(
onEnter: (event) => setHidden(false),
onExit: (event) {
if (!isPopoverOpen) {
setHidden(true);
}
},
opaque: false,
child: child,
);
}
return child;
} }
List<Widget> buildRowChildren() { List<Widget> buildRowChildren() {
@ -251,42 +291,50 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
), ),
); );
} else { } else {
children.add( Widget child = FlowyButton(
AppFlowyPopover( leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
onTap: PlatformExtension.isDesktop
? null
: () => context.push(
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {
MobileEmojiPickerScreen.viewId:
context.read<ViewBloc>().state.view.id,
},
).toString(),
),
);
if (PlatformExtension.isDesktop) {
child = AppFlowyPopover(
onClose: () => isPopoverOpen = false, onClose: () => isPopoverOpen = false,
controller: _popoverController, controller: _popoverController,
offset: const Offset(0, 8), offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(300, 250)), constraints: BoxConstraints.loose(const Size(360, 380)),
child: FlowyButton( child: child,
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true; isPopoverOpen = true;
return EmojiPopover( return FlowyIconPicker(
showRemoveButton: widget.hasIcon, onSelected: (type, value) {
removeIcon: () { widget.onCoverChanged(icon: value);
widget.onCoverChanged(icon: "");
_popoverController.close();
},
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onCoverChanged(icon: emoji.emoji);
_popoverController.close(); _popoverController.close();
}, },
); );
}, },
), );
); }
children.add(child);
} }
return children; return children;
@ -471,27 +519,41 @@ class _DocumentIconState extends State<DocumentIcon> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( Widget child = EmojiIconWidget(
direction: PopoverDirection.bottomWithCenterAligned, emoji: widget.icon,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: BoxConstraints.loose(const Size(320, 380)),
child: EmojiIconWidget(emoji: widget.icon),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: true,
removeIcon: () {
widget.onIconChanged("");
_popoverController.close();
},
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onIconChanged(emoji.emoji);
_popoverController.close();
},
);
},
); );
if (PlatformExtension.isDesktopOrWeb) {
child = AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: BoxConstraints.loose(const Size(360, 380)),
child: child,
popupBuilder: (BuildContext popoverContext) {
return FlowyIconPicker(
onSelected: (type, value) {
widget.onIconChanged(value);
_popoverController.close();
},
);
},
);
} else {
child = GestureDetector(
child: child,
onTap: () => context.push(
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {
MobileEmojiPickerScreen.viewId:
context.read<ViewBloc>().state.view.id,
},
).toString(),
),
);
}
return child;
} }
} }

View File

@ -1,76 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
/// Add icon menu in Header
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Column(
children: [
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onTap: widget.removeIcon),
),
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: buildFlowyEmojiPickerConfig(context),
),
),
],
);
}
}
class DeleteButton extends StatelessWidget {
final VoidCallback onTap;
const DeleteButton({required this.onTap, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 28,
child: FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
),
);
}
}

View File

@ -29,6 +29,10 @@ class EditorStyleCustomizer {
throw UnimplementedError(); throw UnimplementedError();
} }
static EdgeInsets get documentPadding => PlatformExtension.isMobile
? const EdgeInsets.only(left: 20, right: 20)
: const EdgeInsets.only(left: 40, right: 40 + 44);
EditorStyle desktop() { EditorStyle desktop() {
final theme = Theme.of(context); final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize; final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;

View File

@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dar
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart';
import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
@ -47,6 +48,9 @@ GoRouter generateRouter(Widget child) {
// trash // trash
_mobileHomeTrashPageRoute(), _mobileHomeTrashPageRoute(),
// emoji picker
_mobileEmojiPickerPageRoute(),
], ],
// Desktop and Mobile // Desktop and Mobile
@ -200,6 +204,21 @@ GoRoute _mobileHomeTrashPageRoute() {
); );
} }
GoRoute _mobileEmojiPickerPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,
path: MobileEmojiPickerScreen.routeName,
pageBuilder: (context, state) {
final id = state.uri.queryParameters[MobileEmojiPickerScreen.viewId]!;
return MaterialPage(
child: MobileEmojiPickerScreen(
id: id,
),
);
},
);
}
GoRoute _desktopHomeScreenRoute() { GoRoute _desktopHomeScreenRoute() {
return GoRoute( return GoRoute(
path: DesktopHomeScreen.routeName, path: DesktopHomeScreen.routeName,

View File

@ -1,10 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:dartz/dartz.dart';
class ViewBackendService { class ViewBackendService {
static Future<Either<ViewPB, FlowyError>> createView({ static Future<Either<ViewPB, FlowyError>> createView({
@ -149,9 +148,24 @@ class ViewBackendService {
if (isFavorite != null) { if (isFavorite != null) {
payload.isFavorite = isFavorite; payload.isFavorite = isFavorite;
} }
return FolderEventUpdateView(payload).send(); return FolderEventUpdateView(payload).send();
} }
static Future<Either<Unit, FlowyError>> updateViewIcon({
required String viewId,
required String viewIcon,
}) {
final icon = ViewIconPB()
..ty = ViewIconTypePB.Emoji
..value = viewIcon;
final payload = UpdateViewIconPayloadPB.create()
..viewId = viewId
..icon = icon;
return FolderEventUpdateViewIcon(payload).send();
}
// deprecated // deprecated
static Future<Either<Unit, FlowyError>> moveView({ static Future<Either<Unit, FlowyError>> moveView({
required String viewId, required String viewId,

View File

@ -1,10 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
@ -14,6 +15,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d
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/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -36,6 +38,7 @@ class ViewItem extends StatelessWidget {
this.isFirstChild = false, this.isFirstChild = false,
this.isDraggable = true, this.isDraggable = true,
required this.isFeedback, required this.isFeedback,
this.height = 28.0,
}); });
final ViewPB view; final ViewPB view;
@ -67,6 +70,8 @@ class ViewItem extends StatelessWidget {
// identify if the view item is rendered as feedback widget inside DraggableItem // identify if the view item is rendered as feedback widget inside DraggableItem
final bool isFeedback; final bool isFeedback;
final double height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -96,6 +101,7 @@ class ViewItem extends StatelessWidget {
isFirstChild: isFirstChild, isFirstChild: isFirstChild,
isDraggable: isDraggable, isDraggable: isDraggable,
isFeedback: isFeedback, isFeedback: isFeedback,
height: height,
); );
}, },
), ),
@ -121,6 +127,7 @@ class InnerViewItem extends StatelessWidget {
this.onTertiarySelected, this.onTertiarySelected,
this.isFirstChild = false, this.isFirstChild = false,
required this.isFeedback, required this.isFeedback,
required this.height,
}); });
final ViewPB view; final ViewPB view;
@ -140,6 +147,7 @@ class InnerViewItem extends StatelessWidget {
final bool showActions; final bool showActions;
final ViewItemOnSelected onSelected; final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected; final ViewItemOnSelected? onTertiarySelected;
final double height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -155,6 +163,7 @@ class InnerViewItem extends StatelessWidget {
isDraggable: isDraggable, isDraggable: isDraggable,
leftPadding: leftPadding, leftPadding: leftPadding,
isFeedback: isFeedback, isFeedback: isFeedback,
height: height,
); );
// 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
@ -233,6 +242,7 @@ class SingleInnerViewItem extends StatefulWidget {
required this.onSelected, required this.onSelected,
this.onTertiarySelected, this.onTertiarySelected,
required this.isFeedback, required this.isFeedback,
required this.height,
}); });
final ViewPB view; final ViewPB view;
@ -249,12 +259,16 @@ class SingleInnerViewItem extends StatefulWidget {
final ViewItemOnSelected onSelected; final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected; final ViewItemOnSelected? onTertiarySelected;
final FolderCategoryType categoryType; final FolderCategoryType categoryType;
final double height;
@override @override
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState(); State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
} }
class _SingleInnerViewItemState extends State<SingleInnerViewItem> { class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
final controller = PopoverController();
bool isIconPickerOpened = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.isFeedback) { if (widget.isFeedback) {
@ -266,7 +280,8 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
hoverColor: Theme.of(context).colorScheme.secondary, hoverColor: Theme.of(context).colorScheme.secondary,
), ),
resetHoverOnRebuild: widget.showActions, resetHoverOnRebuild: widget.showActions,
buildWhenOnHover: () => !widget.showActions && !_isDragging, buildWhenOnHover: () =>
!widget.showActions && !_isDragging && !isIconPickerOpened,
builder: (_, onHover) => _buildViewItem(onHover), builder: (_, onHover) => _buildViewItem(onHover),
isSelected: () => isSelected: () =>
widget.showActions || widget.showActions ||
@ -279,10 +294,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
// expand icon // expand icon
_buildLeftIcon(), _buildLeftIcon(),
// icon // icon
SizedBox.square( _buildViewIconButton(),
dimension: 16,
child: widget.view.defaultIcon(),
),
const HSpace(5), const HSpace(5),
// title // title
Expanded( Expanded(
@ -309,7 +321,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
onTap: () => widget.onSelected(widget.view), onTap: () => widget.onSelected(widget.view),
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view), onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view),
child: SizedBox( child: SizedBox(
height: 26, height: widget.height,
child: Padding( child: Padding(
padding: EdgeInsets.only(left: widget.level * widget.leftPadding), padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
child: Row( child: Row(
@ -320,6 +332,47 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
); );
} }
Widget _buildViewIconButton() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText(
widget.view.icon.value,
fontSize: 18.0,
)
: SizedBox.square(
dimension: 20.0,
child: widget.view.defaultIcon(),
);
return AppFlowyPopover(
offset: const Offset(20, 0),
controller: controller,
direction: PopoverDirection.rightWithCenterAligned,
constraints: BoxConstraints.loose(const Size(360, 380)),
onClose: () => setState(() {
isIconPickerOpened = false;
}),
child: GestureDetector(
// prevent the tap event from being passed to the parent widget
onTap: () {},
child: FlowyTooltip(
message: LocaleKeys.document_plugins_cover_changeIcon.tr(),
child: icon,
),
),
popupBuilder: (context) {
isIconPickerOpened = true;
return FlowyIconPicker(
onSelected: (_, emoji) {
ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: emoji,
);
controller.close();
},
);
},
);
}
// > button or · button // > button or · button
// show > if the view is expandable. // show > if the view is expandable.
// show · if the view can't contain child views. // show · if the view can't contain child views.

View File

@ -1,11 +1,10 @@
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/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'emoji_picker.dart';
SelectionMenuItem emojiMenuItem = SelectionMenuItem( SelectionMenuItem emojiMenuItem = SelectionMenuItem(
name: 'Emoji', name: 'Emoji',
icon: (editorState, onSelected, style) => SelectableIconWidget( icon: (editorState, onSelected, style) => SelectableIconWidget(
@ -54,7 +53,7 @@ void showEmojiPickerMenu(
), ),
child: EmojiSelectionMenu( child: EmojiSelectionMenu(
onSubmitted: (emoji) { onSubmitted: (emoji) {
editorState.insertTextAtCurrentSelection(emoji.emoji); editorState.insertTextAtCurrentSelection(emoji);
}, },
onExit: () { onExit: () {
// close emoji panel // close emoji panel
@ -73,7 +72,7 @@ class EmojiSelectionMenu extends StatefulWidget {
required this.onExit, required this.onExit,
}) : super(key: key); }) : super(key: key);
final void Function(Emoji emoji) onSubmitted; final void Function(String emoji) onSubmitted;
final void Function() onExit; final void Function() onExit;
@override @override
@ -111,9 +110,10 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EmojiPicker( return FlowyEmojiPicker(
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji), onEmojiSelected: (_, emoji) {
config: buildFlowyEmojiPickerConfig(context), widget.onSubmitted(emoji);
},
); );
} }
} }

View File

@ -22,6 +22,10 @@ class FlowyTextField extends StatefulWidget {
final String? errorText; final String? errorText;
final int maxLines; final int maxLines;
final bool showCounter; final bool showCounter;
final Widget? prefixIcon;
final Widget? suffixIcon;
final BoxConstraints? prefixIconConstraints;
final BoxConstraints? suffixIconConstraints;
const FlowyTextField({ const FlowyTextField({
super.key, super.key,
@ -42,6 +46,10 @@ class FlowyTextField extends StatefulWidget {
this.errorText, this.errorText,
this.maxLines = 1, this.maxLines = 1,
this.showCounter = true, this.showCounter = true,
this.prefixIcon,
this.suffixIcon,
this.prefixIconConstraints,
this.suffixIconConstraints,
}); });
@override @override
@ -55,6 +63,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
@override @override
void initState() { void initState() {
super.initState();
focusNode = widget.focusNode ?? FocusNode(); focusNode = widget.focusNode ?? FocusNode();
focusNode.addListener(notifyDidEndEditing); focusNode.addListener(notifyDidEndEditing);
@ -67,10 +77,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus(); focusNode.requestFocus();
controller.selection = TextSelection.fromPosition( controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length)); TextPosition(offset: controller.text.length),
);
}); });
} }
super.initState();
} }
void _debounceOnChangedText(Duration duration, String text) { void _debounceOnChangedText(Duration duration, String text) {
@ -113,6 +123,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
maxLength: widget.maxLength, maxLength: widget.maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration( decoration: InputDecoration(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58), maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58),
@ -158,6 +169,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
), ),
borderRadius: Corners.s8Border, borderRadius: Corners.s8Border,
), ),
prefixIcon: widget.prefixIcon,
suffixIcon: widget.suffixIcon,
prefixIconConstraints: widget.prefixIconConstraints,
suffixIconConstraints: widget.suffixIconConstraints,
), ),
); );
} }

View File

@ -53,12 +53,11 @@ packages:
appflowy_editor: appflowy_editor:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: appflowy_editor
ref: "7336274" sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad
resolved-ref: "7336274ff90402c8dd790b029e00cac60c580f28" url: "https://pub.dev"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: hosted
source: git version: "1.5.1"
version: "1.5.0"
appflowy_popover: appflowy_popover:
dependency: "direct main" dependency: "direct main"
description: description:
@ -346,6 +345,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0+3" version: "2.0.0+3"
easy_debounce:
dependency: transitive
description:
name: easy_debounce
sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
url: "https://pub.dev"
source: hosted
version: "2.0.3"
easy_localization: easy_localization:
dependency: "direct main" dependency: "direct main"
description: description:
@ -362,6 +369,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.2" version: "0.0.2"
emoji_mart:
dependency: "direct main"
description:
path: "."
ref: "067f718"
resolved-ref: "067f7188965c8fcb7be02ce174ce2b6757f288ee"
url: "https://github.com/LucasXu0/emoji_mart.git"
source: git
version: "0.0.1"
envied: envied:
dependency: "direct main" dependency: "direct main"
description: description:
@ -533,6 +549,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
flutter_sticky_header:
dependency: transitive
description:
name: flutter_sticky_header
sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -571,10 +595,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.4.1"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -1390,6 +1414,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
sliver_tools:
dependency: transitive
description:
name: sliver_tools
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
url: "https://pub.dev"
source: hosted
version: "0.2.12"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -1728,6 +1760,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
value_layout_builder:
dependency: transitive
description:
name: value_layout_builder
sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
vector_graphics: vector_graphics:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,10 +44,7 @@ dependencies:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-board.git url: https://github.com/AppFlowy-IO/appflowy-board.git
ref: 6aba8dd ref: 6aba8dd
appflowy_editor: appflowy_editor: ^1.5.1
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "7336274"
appflowy_popover: appflowy_popover:
path: packages/appflowy_popover path: packages/appflowy_popover
@ -110,6 +107,10 @@ dependencies:
go_router: ^10.1.2 go_router: ^10.1.2
string_validator: ^1.0.0 string_validator: ^1.0.0
unsplash_client: ^2.1.1 unsplash_client: ^2.1.1
emoji_mart:
git:
url: https://github.com/LucasXu0/emoji_mart.git
ref: "067f718"
# Notifications # Notifications
# TODO: Consider implementing custom package # TODO: Consider implementing custom package

View File

@ -619,13 +619,14 @@
"add": "Add", "add": "Add",
"back": "Back", "back": "Back",
"saveToGallery": "Save to gallery", "saveToGallery": "Save to gallery",
"removeIcon": "Remove Icon", "removeIcon": "Remove icon",
"pasteImageUrl": "Paste image URL", "pasteImageUrl": "Paste image URL",
"or": "OR", "or": "OR",
"pickFromFiles": "Pick from files", "pickFromFiles": "Pick from files",
"couldNotFetchImage": "Could not fetch image", "couldNotFetchImage": "Could not fetch image",
"imageSavingFailed": "Image Saving Failed", "imageSavingFailed": "Image Saving Failed",
"addIcon": "Add Icon", "addIcon": "Add icon",
"changeIcon": "Change icon",
"coverRemoveAlert": "It will be removed from cover after it is deleted.", "coverRemoveAlert": "It will be removed from cover after it is deleted.",
"alertDialogConfirmation": "Are you sure, you want to continue?" "alertDialogConfirmation": "Are you sure, you want to continue?"
}, },
@ -818,6 +819,7 @@
"gray": "Gray" "gray": "Gray"
}, },
"emoji": { "emoji": {
"emojiTab": "Emoji",
"search": "Search emoji", "search": "Search emoji",
"noRecent": "No recent emoji", "noRecent": "No recent emoji",
"noEmojiFound": "No emoji found", "noEmojiFound": "No emoji found",
@ -837,6 +839,14 @@
"flags": "Flags", "flags": "Flags",
"nature": "Nature", "nature": "Nature",
"frequentlyUsed": "Frequently Used" "frequentlyUsed": "Frequently Used"
},
"skinTone": {
"default": "Default",
"light": "Light",
"mediumLight": "Medium-Light",
"medium": "Medium",
"mediumDark": "Medium-Dark",
"dark": "Dark"
} }
}, },
"inlineActions": { "inlineActions": {