mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
21d34d1fe0
commit
c34a7a92fb
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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));
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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('');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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 '✋🏿';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart
Normal file
121
frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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: () {},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user