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:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -70,11 +69,6 @@ jobs:
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: appflowy-lib-cache
|
||||
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
@ -151,7 +145,6 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -162,11 +155,6 @@ jobs:
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: appflowy-lib-cache
|
||||
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
@ -257,7 +245,6 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@ -266,11 +253,6 @@ jobs:
|
||||
targets: ${{ matrix.job.targets }}
|
||||
components: rustfmt
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: appflowy-lib-cache
|
||||
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
@ -366,7 +348,6 @@ jobs:
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -377,11 +358,6 @@ jobs:
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: appflowy-lib-cache
|
||||
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
|
@ -42,7 +42,6 @@ void main() {
|
||||
await tester.hoverRowBanner();
|
||||
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// After select the emoji, the EmojiButton will show up
|
||||
@ -60,12 +59,10 @@ void main() {
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.hoverRowBanner();
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// Update existing selected emoji
|
||||
await tester.tapButton(find.byType(EmojiButton));
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😅');
|
||||
|
||||
// The emoji already displayed in the row banner
|
||||
@ -89,7 +86,6 @@ void main() {
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.hoverRowBanner();
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// 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: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:integration_test/integration_test.dart';
|
||||
|
||||
@ -61,7 +65,6 @@ void main() {
|
||||
|
||||
// Insert a document icon
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
tester.expectToSeeDocumentIcon('😀');
|
||||
|
||||
@ -73,13 +76,11 @@ void main() {
|
||||
// Add the icon back for further testing
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
tester.expectToSeeDocumentIcon('😀');
|
||||
|
||||
// Change the document icon
|
||||
await tester.editor.tapOnIconWidget();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😅');
|
||||
tester.expectToSeeDocumentIcon('😅');
|
||||
|
||||
@ -102,7 +103,6 @@ void main() {
|
||||
|
||||
// Insert a document icon
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// Insert a document cover
|
||||
@ -116,5 +116,46 @@ void main() {
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
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:io';
|
||||
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.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:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -81,9 +83,15 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
final finder = find.byType(GoButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
if (isCloudEnabled) {
|
||||
final finder = find.byType(SignInAnonymousButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
} else {
|
||||
final finder = find.byType(GoButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
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/startup/startup.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/view/draggable_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
@ -27,8 +28,15 @@ import 'util.dart';
|
||||
extension CommonOperations on WidgetTester {
|
||||
/// Tap the GetStart button on the launch page.
|
||||
Future<void> tapGoButton() async {
|
||||
// local version
|
||||
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) {
|
||||
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/tar_bar_add_button.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/cells/cells.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_property.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/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
@ -618,7 +617,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
Future<void> openEmojiPicker() async {
|
||||
await tapButton(find.byType(EmojiPickerButton));
|
||||
await tapButton(find.byType(EmojiSelectionMenu));
|
||||
}
|
||||
|
||||
Future<void> tapDateCellInRowDetailPage() async {
|
||||
|
@ -1,15 +1,18 @@
|
||||
import 'dart:ui';
|
||||
|
||||
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/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/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_popover.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -54,7 +57,18 @@ class EditorOperations {
|
||||
await tester.tapButtonWithName(
|
||||
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
|
||||
@ -62,7 +76,10 @@ class EditorOperations {
|
||||
Finder button =
|
||||
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
|
||||
if (isInPicker) {
|
||||
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
|
||||
button = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: button,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.tapButton(button);
|
||||
|
@ -1,15 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
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 {
|
||||
final emojiWidget = find.text(emoji);
|
||||
await tapButton(emojiWidget);
|
||||
|
@ -108,6 +108,13 @@ extension Expectation on WidgetTester {
|
||||
expect(iconWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
void expectDocumentIconNotNull() {
|
||||
final iconWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty,
|
||||
);
|
||||
expect(iconWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
void expectToSeeDocumentCover(CoverType type) {
|
||||
final findCover = find.byWidgetPredicate(
|
||||
(widget) => widget is DocumentCover && widget.coverType == type,
|
||||
@ -193,4 +200,13 @@ extension Expectation on WidgetTester {
|
||||
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) {
|
||||
final icon = view?.icon.value;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: FlowyText.semibold(
|
||||
view?.name ?? widget.title ?? '',
|
||||
fontSize: 14.0,
|
||||
title: Row(
|
||||
children: [
|
||||
if (icon != null)
|
||||
FlowyText(
|
||||
'$icon ',
|
||||
fontSize: 22.0,
|
||||
),
|
||||
FlowyText.regular(
|
||||
view?.name ?? widget.title ?? '',
|
||||
fontSize: 14.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: AppBarBackButton(
|
||||
onTap: () => context.pop(),
|
||||
|
@ -303,11 +303,8 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
_buildLeftIcon(),
|
||||
const HSpace(4),
|
||||
// icon
|
||||
SizedBox.square(
|
||||
dimension: 22,
|
||||
child: widget.view.defaultIcon(),
|
||||
),
|
||||
const HSpace(12),
|
||||
_buildViewIconButton(),
|
||||
const HSpace(8),
|
||||
// title
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
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
|
||||
// show > if the view is expandable.
|
||||
// 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,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300),
|
||||
popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
|
||||
context
|
||||
.read<RowBannerBloc>()
|
||||
.add(RowBannerEvent.setIcon(emoji.emoji));
|
||||
context.read<RowBannerBloc>().add(RowBannerEvent.setIcon(emoji));
|
||||
widget.popoverController.close();
|
||||
}),
|
||||
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;
|
||||
|
||||
class EmojiButton extends StatelessWidget {
|
||||
@ -286,12 +285,9 @@ class RemoveEmojiButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: EmojiSelectionMenu(
|
||||
onSubmitted: onSubmitted,
|
||||
onExit: () {},
|
||||
),
|
||||
return EmojiSelectionMenu(
|
||||
onSubmitted: onSubmitted,
|
||||
onExit: () {},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import 'package:appflowy/startup/startup.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_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||
@ -111,9 +112,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
// the 44 is the width of the left action list
|
||||
padding: PlatformExtension.isMobile
|
||||
? const EdgeInsets.only(left: 20, right: 20)
|
||||
: const EdgeInsets.only(left: 40, right: 40 + 44),
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildCoverAndIcon(context),
|
||||
);
|
||||
@ -140,6 +139,13 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
return DocumentHeaderNodeWidget(
|
||||
node: page,
|
||||
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 double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(Emoji emoji, PopoverController controller) onSubmitted;
|
||||
final void Function(String emoji, PopoverController controller) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
|
@ -186,7 +186,7 @@ class _CalloutBlockComponentWidgetState
|
||||
), // force to refresh the popover state
|
||||
emoji: emoji,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji.emoji);
|
||||
setEmoji(emoji);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
|
@ -2,17 +2,23 @@ import 'dart:io';
|
||||
|
||||
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/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_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'cover_editor.dart';
|
||||
import 'emoji_icon_widget.dart';
|
||||
import 'emoji_popover.dart';
|
||||
|
||||
const double kCoverHeight = 250.0;
|
||||
const double kIconHeight = 60.0;
|
||||
@ -45,13 +51,17 @@ enum CoverType {
|
||||
|
||||
class DocumentHeaderNodeWidget extends StatefulWidget {
|
||||
const DocumentHeaderNodeWidget({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
super.key,
|
||||
required this.onIconChanged,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
final void Function(String icon) onIconChanged;
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<DocumentHeaderNodeWidget> createState() =>
|
||||
@ -64,19 +74,33 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
|
||||
);
|
||||
String? get coverDetails =>
|
||||
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];
|
||||
String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];
|
||||
bool get hasIcon =>
|
||||
widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false;
|
||||
String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];
|
||||
bool get hasIcon => viewIcon.isNotEmpty;
|
||||
bool get hasCover => coverType != CoverType.none;
|
||||
|
||||
String viewIcon = '';
|
||||
late final ViewListener viewListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final value = widget.view.icon.value;
|
||||
viewIcon = value.isNotEmpty ? value : icon ?? '';
|
||||
widget.node.addListener(_reload);
|
||||
viewListener = ViewListener(
|
||||
viewId: widget.view.id,
|
||||
)..start(
|
||||
onViewUpdated: (p0) {
|
||||
setState(() {
|
||||
viewIcon = p0.icon.value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
viewListener.stop();
|
||||
widget.node.removeListener(_reload);
|
||||
super.dispose();
|
||||
}
|
||||
@ -108,7 +132,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
|
||||
),
|
||||
if (hasIcon)
|
||||
Positioned(
|
||||
left: 80,
|
||||
left: PlatformExtension.isDesktopOrWeb ? 80 : 20,
|
||||
// if hasCover, there shouldn't be icons present so the icon can
|
||||
// be closer to the bottom.
|
||||
bottom:
|
||||
@ -116,8 +140,10 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
|
||||
child: DocumentIcon(
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
icon: icon,
|
||||
onIconChanged: (icon) => _saveCover(icon: icon),
|
||||
icon: viewIcon,
|
||||
onIconChanged: (icon) async {
|
||||
_saveCover(icon: icon);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -153,6 +179,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
|
||||
}
|
||||
if (icon != null) {
|
||||
attributes[DocumentHeaderBlockKeys.icon] = icon;
|
||||
widget.onIconChanged(icon);
|
||||
}
|
||||
|
||||
transaction.updateNode(widget.node, attributes);
|
||||
@ -188,29 +215,42 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
isHidden = PlatformExtension.isDesktopOrWeb;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setHidden(false),
|
||||
onExit: (event) {
|
||||
if (!isPopoverOpen) {
|
||||
setHidden(true);
|
||||
}
|
||||
},
|
||||
opaque: false,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: buildRowChildren(),
|
||||
),
|
||||
Widget child = Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
width: double.infinity,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
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() {
|
||||
@ -251,42 +291,50 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(
|
||||
AppFlowyPopover(
|
||||
Widget child = FlowyButton(
|
||||
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,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 250)),
|
||||
child: FlowyButton(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
isPopoverOpen = true;
|
||||
return EmojiPopover(
|
||||
showRemoveButton: widget.hasIcon,
|
||||
removeIcon: () {
|
||||
widget.onCoverChanged(icon: "");
|
||||
_popoverController.close();
|
||||
},
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
onEmojiChanged: (Emoji emoji) {
|
||||
widget.onCoverChanged(icon: emoji.emoji);
|
||||
return FlowyIconPicker(
|
||||
onSelected: (type, value) {
|
||||
widget.onCoverChanged(icon: value);
|
||||
_popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
return children;
|
||||
@ -471,27 +519,41 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
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();
|
||||
},
|
||||
);
|
||||
},
|
||||
Widget child = EmojiIconWidget(
|
||||
emoji: widget.icon,
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
static EdgeInsets get documentPadding => PlatformExtension.isMobile
|
||||
? const EdgeInsets.only(left: 20, right: 20)
|
||||
: const EdgeInsets.only(left: 40, right: 40 + 44);
|
||||
|
||||
EditorStyle desktop() {
|
||||
final theme = Theme.of(context);
|
||||
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/favorite/mobile_favorite_page.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/tasks/app_widget.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
@ -47,6 +48,9 @@ GoRouter generateRouter(Widget child) {
|
||||
|
||||
// trash
|
||||
_mobileHomeTrashPageRoute(),
|
||||
|
||||
// emoji picker
|
||||
_mobileEmojiPickerPageRoute(),
|
||||
],
|
||||
|
||||
// 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() {
|
||||
return GoRoute(
|
||||
path: DesktopHomeScreen.routeName,
|
||||
|
@ -1,10 +1,9 @@
|
||||
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/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 {
|
||||
static Future<Either<ViewPB, FlowyError>> createView({
|
||||
@ -149,9 +148,24 @@ class ViewBackendService {
|
||||
if (isFavorite != null) {
|
||||
payload.isFavorite = isFavorite;
|
||||
}
|
||||
|
||||
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
|
||||
static Future<Either<Unit, FlowyError>> moveView({
|
||||
required String viewId,
|
||||
|
@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/workspace/application/favorite/favorite_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/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/presentation/home/menu/menu_shared_state.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/widgets/dialogs.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
@ -36,6 +38,7 @@ class ViewItem extends StatelessWidget {
|
||||
this.isFirstChild = false,
|
||||
this.isDraggable = true,
|
||||
required this.isFeedback,
|
||||
this.height = 28.0,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
@ -67,6 +70,8 @@ class ViewItem extends StatelessWidget {
|
||||
// identify if the view item is rendered as feedback widget inside DraggableItem
|
||||
final bool isFeedback;
|
||||
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
@ -96,6 +101,7 @@ class ViewItem extends StatelessWidget {
|
||||
isFirstChild: isFirstChild,
|
||||
isDraggable: isDraggable,
|
||||
isFeedback: isFeedback,
|
||||
height: height,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -121,6 +127,7 @@ class InnerViewItem extends StatelessWidget {
|
||||
this.onTertiarySelected,
|
||||
this.isFirstChild = false,
|
||||
required this.isFeedback,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
@ -140,6 +147,7 @@ class InnerViewItem extends StatelessWidget {
|
||||
final bool showActions;
|
||||
final ViewItemOnSelected onSelected;
|
||||
final ViewItemOnSelected? onTertiarySelected;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -155,6 +163,7 @@ class InnerViewItem extends StatelessWidget {
|
||||
isDraggable: isDraggable,
|
||||
leftPadding: leftPadding,
|
||||
isFeedback: isFeedback,
|
||||
height: height,
|
||||
);
|
||||
|
||||
// if the view is expanded and has child views, render its child views
|
||||
@ -233,6 +242,7 @@ class SingleInnerViewItem extends StatefulWidget {
|
||||
required this.onSelected,
|
||||
this.onTertiarySelected,
|
||||
required this.isFeedback,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
@ -249,12 +259,16 @@ class SingleInnerViewItem extends StatefulWidget {
|
||||
final ViewItemOnSelected onSelected;
|
||||
final ViewItemOnSelected? onTertiarySelected;
|
||||
final FolderCategoryType categoryType;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
|
||||
}
|
||||
|
||||
class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
final controller = PopoverController();
|
||||
bool isIconPickerOpened = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isFeedback) {
|
||||
@ -266,7 +280,8 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
hoverColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
resetHoverOnRebuild: widget.showActions,
|
||||
buildWhenOnHover: () => !widget.showActions && !_isDragging,
|
||||
buildWhenOnHover: () =>
|
||||
!widget.showActions && !_isDragging && !isIconPickerOpened,
|
||||
builder: (_, onHover) => _buildViewItem(onHover),
|
||||
isSelected: () =>
|
||||
widget.showActions ||
|
||||
@ -279,10 +294,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
// expand icon
|
||||
_buildLeftIcon(),
|
||||
// icon
|
||||
SizedBox.square(
|
||||
dimension: 16,
|
||||
child: widget.view.defaultIcon(),
|
||||
),
|
||||
_buildViewIconButton(),
|
||||
const HSpace(5),
|
||||
// title
|
||||
Expanded(
|
||||
@ -309,7 +321,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
onTap: () => widget.onSelected(widget.view),
|
||||
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view),
|
||||
child: SizedBox(
|
||||
height: 26,
|
||||
height: widget.height,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
|
||||
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
|
||||
// show > if the view is expandable.
|
||||
// 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_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
name: 'Emoji',
|
||||
icon: (editorState, onSelected, style) => SelectableIconWidget(
|
||||
@ -54,7 +53,7 @@ void showEmojiPickerMenu(
|
||||
),
|
||||
child: EmojiSelectionMenu(
|
||||
onSubmitted: (emoji) {
|
||||
editorState.insertTextAtCurrentSelection(emoji.emoji);
|
||||
editorState.insertTextAtCurrentSelection(emoji);
|
||||
},
|
||||
onExit: () {
|
||||
// close emoji panel
|
||||
@ -73,7 +72,7 @@ class EmojiSelectionMenu extends StatefulWidget {
|
||||
required this.onExit,
|
||||
}) : super(key: key);
|
||||
|
||||
final void Function(Emoji emoji) onSubmitted;
|
||||
final void Function(String emoji) onSubmitted;
|
||||
final void Function() onExit;
|
||||
|
||||
@override
|
||||
@ -111,9 +110,10 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
|
||||
config: buildFlowyEmojiPickerConfig(context),
|
||||
return FlowyEmojiPicker(
|
||||
onEmojiSelected: (_, emoji) {
|
||||
widget.onSubmitted(emoji);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ class FlowyTextField extends StatefulWidget {
|
||||
final String? errorText;
|
||||
final int maxLines;
|
||||
final bool showCounter;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final BoxConstraints? prefixIconConstraints;
|
||||
final BoxConstraints? suffixIconConstraints;
|
||||
|
||||
const FlowyTextField({
|
||||
super.key,
|
||||
@ -42,6 +46,10 @@ class FlowyTextField extends StatefulWidget {
|
||||
this.errorText,
|
||||
this.maxLines = 1,
|
||||
this.showCounter = true,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.prefixIconConstraints,
|
||||
this.suffixIconConstraints,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -55,6 +63,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
focusNode = widget.focusNode ?? FocusNode();
|
||||
focusNode.addListener(notifyDidEndEditing);
|
||||
|
||||
@ -67,10 +77,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
controller.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: controller.text.length));
|
||||
TextPosition(offset: controller.text.length),
|
||||
);
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _debounceOnChangedText(Duration duration, String text) {
|
||||
@ -113,6 +123,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
maxLength: widget.maxLength,
|
||||
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
||||
style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58),
|
||||
@ -158,6 +169,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
prefixIcon: widget.prefixIcon,
|
||||
suffixIcon: widget.suffixIcon,
|
||||
prefixIconConstraints: widget.prefixIconConstraints,
|
||||
suffixIconConstraints: widget.suffixIconConstraints,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -53,12 +53,11 @@ packages:
|
||||
appflowy_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "7336274"
|
||||
resolved-ref: "7336274ff90402c8dd790b029e00cac60c580f28"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "1.5.0"
|
||||
name: appflowy_editor
|
||||
sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -346,6 +345,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -362,6 +369,15 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -533,6 +549,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -571,10 +595,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338
|
||||
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.4.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1390,6 +1414,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1728,6 +1760,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -44,10 +44,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: 6aba8dd
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "7336274"
|
||||
appflowy_editor: ^1.5.1
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
@ -110,6 +107,10 @@ dependencies:
|
||||
go_router: ^10.1.2
|
||||
string_validator: ^1.0.0
|
||||
unsplash_client: ^2.1.1
|
||||
emoji_mart:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/emoji_mart.git
|
||||
ref: "067f718"
|
||||
|
||||
# Notifications
|
||||
# TODO: Consider implementing custom package
|
||||
|
@ -619,13 +619,14 @@
|
||||
"add": "Add",
|
||||
"back": "Back",
|
||||
"saveToGallery": "Save to gallery",
|
||||
"removeIcon": "Remove Icon",
|
||||
"removeIcon": "Remove icon",
|
||||
"pasteImageUrl": "Paste image URL",
|
||||
"or": "OR",
|
||||
"pickFromFiles": "Pick from files",
|
||||
"couldNotFetchImage": "Could not fetch image",
|
||||
"imageSavingFailed": "Image Saving Failed",
|
||||
"addIcon": "Add Icon",
|
||||
"addIcon": "Add icon",
|
||||
"changeIcon": "Change icon",
|
||||
"coverRemoveAlert": "It will be removed from cover after it is deleted.",
|
||||
"alertDialogConfirmation": "Are you sure, you want to continue?"
|
||||
},
|
||||
@ -818,6 +819,7 @@
|
||||
"gray": "Gray"
|
||||
},
|
||||
"emoji": {
|
||||
"emojiTab": "Emoji",
|
||||
"search": "Search emoji",
|
||||
"noRecent": "No recent emoji",
|
||||
"noEmojiFound": "No emoji found",
|
||||
@ -837,6 +839,14 @@
|
||||
"flags": "Flags",
|
||||
"nature": "Nature",
|
||||
"frequentlyUsed": "Frequently Used"
|
||||
},
|
||||
"skinTone": {
|
||||
"default": "Default",
|
||||
"light": "Light",
|
||||
"mediumLight": "Medium-Light",
|
||||
"medium": "Medium",
|
||||
"mediumDark": "Medium-Dark",
|
||||
"dark": "Dark"
|
||||
}
|
||||
},
|
||||
"inlineActions": {
|
||||
|
Loading…
Reference in New Issue
Block a user