Compare commits
7
Commits
select_source
...
0.7.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63f767926 | ||
|
|
8826e479eb | ||
|
|
8e4fe3d559 | ||
|
|
e2ee11e48a | ||
|
|
bdc0fa1f2a | ||
|
|
3836545682 | ||
|
|
02eb0e0b83 |
No files matched your search
+18
-2
@@ -1,8 +1,24 @@
|
||||
# Release Notes
|
||||
## Version 0.7.9 - 25/12/2024
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
|
||||
+66
-6
@@ -5,6 +5,7 @@ import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -102,8 +103,7 @@ void main() {
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('only display one menu item in the workspace menu',
|
||||
(tester) async {
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
@@ -128,6 +128,8 @@ void main() {
|
||||
final workspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
|
||||
);
|
||||
|
||||
// the workspace menu shouldn't conflict with logout
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
@@ -136,15 +138,73 @@ void main() {
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
final logoutButton = find.byType(WorkspaceMoreButton);
|
||||
await tester.tapButton(logoutButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
|
||||
expect(moreButton, findsNothing);
|
||||
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
|
||||
expect(moreButton, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more action button for the same workspace shouldn't do
|
||||
// anything
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
// click it again
|
||||
await tester.tapButton(moreButton);
|
||||
|
||||
// nothing should happen
|
||||
expect(
|
||||
find.text(LocaleKeys.button_rename.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more button of another workspace should close the menu
|
||||
// for this one
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final otherWorspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
|
||||
);
|
||||
final otherMoreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
otherWorspaceItem,
|
||||
onHover: () async {
|
||||
expect(otherMoreButton, findsOneWidget);
|
||||
await tester.tapButton(otherMoreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
expect(moreButton, findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+9
-1
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
|
||||
+10
-3
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ void main() {
|
||||
|
||||
// open settings page to check the result
|
||||
await tester.tapButton(settingsButton);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
expect(
|
||||
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -12,7 +13,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Sidebar view item tests', () {
|
||||
testWidgets('Access view item context menu by right click', (tester) async {
|
||||
|
||||
+43
-5
@@ -274,7 +274,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
onPressed: () async {
|
||||
final documentId = getOpenedDocumentId();
|
||||
if (documentId != null) {
|
||||
await onAddToExistingPage(documentId);
|
||||
await onAddToExistingPage(context, documentId);
|
||||
await forceReloadAndUpdateSelection(documentId);
|
||||
} else {
|
||||
widget.onOverrideVisibility?.call(true);
|
||||
@@ -298,9 +298,8 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
},
|
||||
onAddToExistingPage: (documentId) async {
|
||||
popoverController.close();
|
||||
await onAddToExistingPage(documentId);
|
||||
final view =
|
||||
await ViewBackendService.getView(documentId).toNullable();
|
||||
final view = await onAddToExistingPage(context, documentId);
|
||||
|
||||
if (context.mounted) {
|
||||
openPageFromMessage(context, view);
|
||||
}
|
||||
@@ -309,12 +308,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onAddToExistingPage(String documentId) async {
|
||||
Future<ViewPB?> onAddToExistingPage(
|
||||
BuildContext context,
|
||||
String documentId,
|
||||
) async {
|
||||
await ChatEditDocumentService.addMessageToPage(
|
||||
documentId,
|
||||
widget.textMessage,
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final view = await ViewBackendService.getView(documentId).toNullable();
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
void addMessageToNewPage(BuildContext context) async {
|
||||
@@ -327,12 +334,43 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
chatView.parentViewId,
|
||||
[widget.textMessage],
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSaveMessageSuccessToast(context, newView);
|
||||
openPageFromMessage(context, newView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
showToastNotification(
|
||||
context,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' ',
|
||||
),
|
||||
TextSpan(
|
||||
text: view.nameOrDefault,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFFFFFFFF),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> forceReloadAndUpdateSelection(String documentId) async {
|
||||
final bloc = DocumentBloc.findOpen(documentId);
|
||||
if (bloc == null) {
|
||||
|
||||
@@ -90,6 +90,7 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
|
||||
data == null) {
|
||||
return _MetadataButton(
|
||||
name: m.name,
|
||||
onTap: () => widget.onSelectedMetadata?.call(m),
|
||||
);
|
||||
}
|
||||
return BlocProvider(
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class _ChatErrorMessageWidgetState extends State<ChatErrorMessageWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.warning_filled_s,
|
||||
FlowySvgs.toast_error_filled_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
@@ -10,6 +13,11 @@ import 'package:universal_platform/universal_platform.dart';
|
||||
/// on mobile
|
||||
void openPageFromMessage(BuildContext context, ViewPB? view) {
|
||||
if (view == null) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
|
||||
@@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
EmojiData? kCachedEmojiData;
|
||||
const _kRecentEmojiCategoryId = 'Recent';
|
||||
|
||||
class EmojiPickerResult {
|
||||
EmojiPickerResult({
|
||||
required this.emojiId,
|
||||
required this.emoji,
|
||||
this.isRandom = false,
|
||||
});
|
||||
|
||||
final String emojiId;
|
||||
final String emoji;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
class FlowyEmojiPicker extends StatefulWidget {
|
||||
const FlowyEmojiPicker({
|
||||
super.key,
|
||||
@@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget {
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final EmojiSelectedCallback onEmojiSelected;
|
||||
final ValueChanged<EmojiPickerResult> onEmojiSelected;
|
||||
final int emojiPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
@@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
|
||||
),
|
||||
onEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(id, emoji);
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
onSkinToneChanged: (value) {
|
||||
skinTone.value = value;
|
||||
},
|
||||
onRandomEmojiSelected: widget.onEmojiSelected,
|
||||
onRandomEmojiSelected: (id, emoji) {
|
||||
widget.onEmojiSelected.call(
|
||||
EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),
|
||||
);
|
||||
RecentIcons.putEmoji(id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
||||
const MobileEmojiPickerScreen({
|
||||
super.key,
|
||||
this.title,
|
||||
this.selectedType,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final PickerTabType? selectedType;
|
||||
final String? title;
|
||||
final List<PickerTabType> tabs;
|
||||
|
||||
static const routeName = '/emoji_picker';
|
||||
static const pageTitle = 'title';
|
||||
static const iconSelectedType = 'iconSelectedType';
|
||||
static const selectTabs = 'tabs';
|
||||
|
||||
@override
|
||||
@@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget {
|
||||
body: SafeArea(
|
||||
child: FlowyIconEmojiPicker(
|
||||
tabs: tabs,
|
||||
initialType: selectedType,
|
||||
onSelectedEmoji: (r) {
|
||||
context.pop<EmojiIconData>(r);
|
||||
context.pop<EmojiIconData>(r.data);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+3
-3
@@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State<RenameRowPopover> {
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 18),
|
||||
defaultIcon: const FlowySvg(FlowySvgs.document_s),
|
||||
onSubmitted: (emoji, _) {
|
||||
widget.onUpdateIcon(emoji);
|
||||
PopoverContainer.of(context).close();
|
||||
onSubmitted: (r, _) {
|
||||
widget.onUpdateIcon(r.data);
|
||||
if (!r.keepOpen) PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
const HSpace(6),
|
||||
|
||||
@@ -505,6 +505,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
Position(path: lastNode.path),
|
||||
);
|
||||
}
|
||||
|
||||
transaction.customSelectionType = SelectionType.inline;
|
||||
transaction.reason = SelectionUpdateReason.uiEvent;
|
||||
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
@@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final Size emojiPickerSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
final Widget? defaultIcon;
|
||||
final Offset? offset;
|
||||
@@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
|
||||
height: emojiPickerSize.height,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: emoji.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
onSubmitted(r, popoverController);
|
||||
},
|
||||
@@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
||||
|
||||
final EmojiIconData emoji;
|
||||
final double emojiSize;
|
||||
final void Function(EmojiIconData emoji, PopoverController? controller)
|
||||
onSubmitted;
|
||||
final void Function(
|
||||
SelectedEmojiIconResult result,
|
||||
PopoverController? controller,
|
||||
) onSubmitted;
|
||||
final String? title;
|
||||
final bool enable;
|
||||
final EdgeInsets? margin;
|
||||
@@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget {
|
||||
final result = await context.push<EmojiIconData>(
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {MobileEmojiPickerScreen.pageTitle: title},
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle: title,
|
||||
MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
onSubmitted(result, null);
|
||||
onSubmitted(result.toSelectedResult(), null);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
||||
+3
-2
@@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
if (!r.keepOpen) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+14
-10
@@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
isPopoverOpen = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onIconOrCoverChanged(icon: result);
|
||||
_popoverController.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onIconOrCoverChanged(icon: r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -838,9 +838,7 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = EmojiIconWidget(
|
||||
emoji: widget.icon,
|
||||
);
|
||||
Widget child = EmojiIconWidget(emoji: widget.icon);
|
||||
|
||||
if (UniversalPlatform.isDesktopOrWeb) {
|
||||
child = AppFlowyPopover(
|
||||
@@ -852,9 +850,10 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onChangeIcon(result);
|
||||
_popoverController.close();
|
||||
initialType: widget.icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onChangeIcon(r.data);
|
||||
if (!r.keepOpen) _popoverController.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -864,7 +863,12 @@ class _DocumentIconState extends State<DocumentIcon> {
|
||||
child: child,
|
||||
onTap: () async {
|
||||
final result = await context.push<EmojiIconData>(
|
||||
MobileEmojiPickerScreen.routeName,
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
if (result != null) {
|
||||
widget.onChangeIcon(result);
|
||||
|
||||
+21
-9
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
@@ -67,24 +68,35 @@ class RawEmojiIconWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultEmoji = EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
final defaultEmoji = SizedBox(
|
||||
width: emojiSize,
|
||||
child: EmojiText(
|
||||
emoji: '❓',
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
try {
|
||||
switch (emoji.type) {
|
||||
case FlowyIconType.emoji:
|
||||
return EmojiText(
|
||||
emoji: emoji.emoji,
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
return SizedBox(
|
||||
width: emojiSize,
|
||||
child: EmojiText(
|
||||
emoji: emoji.emoji,
|
||||
fontSize: emojiSize,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
case FlowyIconType.icon:
|
||||
final iconData = IconsData.fromJson(jsonDecode(emoji.emoji));
|
||||
|
||||
/// Under the same width conditions, icons on macOS seem to appear
|
||||
/// larger than emojis, so 0.9 is used here to slightly reduce the
|
||||
/// size of the icons
|
||||
final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize;
|
||||
return IconWidget(
|
||||
data: iconData,
|
||||
size: emojiSize,
|
||||
size: iconSize,
|
||||
);
|
||||
default:
|
||||
return defaultEmoji;
|
||||
|
||||
+11
-1
@@ -20,6 +20,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
abstract class AppFlowyMobileToolbarWidgetService {
|
||||
void closeItemMenu();
|
||||
|
||||
void closeKeyboard();
|
||||
|
||||
PropertyValueNotifier<bool> get showMenuNotifier;
|
||||
@@ -179,7 +180,13 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
// but in this case, we don't want to update the cached keyboard height.
|
||||
// This is because we want to keep the same height when the menu is shown.
|
||||
bool canUpdateCachedKeyboardHeight = true;
|
||||
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
|
||||
|
||||
/// when the [_MobileToolbar] disposed before the keyboard height can be updated in time,
|
||||
/// there will be an issue with the height being 0
|
||||
/// this is used to globally record the height.
|
||||
static double _globalCachedKeyboardHeight = 0.0;
|
||||
ValueNotifier<double> cachedKeyboardHeight =
|
||||
ValueNotifier(_globalCachedKeyboardHeight);
|
||||
|
||||
// used to check if click the same item again
|
||||
int? selectedMenuIndex;
|
||||
@@ -408,6 +415,9 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (keyboardHeight > 0) {
|
||||
_globalCachedKeyboardHeight = keyboardHeight;
|
||||
}
|
||||
return SizedBox(
|
||||
height: keyboardHeight,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
|
||||
+5
-4
@@ -35,7 +35,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
builder: (context, state) {
|
||||
final icon = state.icon ?? EmojiIconData.none();
|
||||
return GestureDetector(
|
||||
onTap: () => _showIconSelector(context),
|
||||
onTap: () => _showIconSelector(context, icon),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 52,
|
||||
@@ -66,7 +66,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showIconSelector(BuildContext context) {
|
||||
void _showIconSelector(BuildContext context, EmojiIconData icon) {
|
||||
Navigator.pop(context);
|
||||
final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
|
||||
..add(const PageStyleIconEvent.initial());
|
||||
@@ -85,11 +85,12 @@ class _PageStyleIconState extends State<PageStyleIcon> {
|
||||
value: pageStyleIconBloc,
|
||||
child: Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
initialType: icon.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
pageStyleIconBloc.add(
|
||||
PageStyleIconEvent.updateIcon(r, true),
|
||||
PageStyleIconEvent.updateIcon(r.data, true),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
if (!r.keepOpen) Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+241
@@ -104,6 +104,18 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -112,6 +124,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -167,6 +187,18 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
comparator: (iKey, index) => iKey >= index,
|
||||
);
|
||||
|
||||
final bool distributeColumnWidthsEvenly =
|
||||
attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ??
|
||||
false;
|
||||
@@ -189,6 +221,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -238,6 +278,18 @@ extension TableMapOperation on Node {
|
||||
index,
|
||||
);
|
||||
|
||||
final (rowBoldAttributes, duplicatedRowBoldAttribute) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
);
|
||||
|
||||
final (rowTextColors, duplicatedRowTextColor) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -248,6 +300,16 @@ extension TableMapOperation on Node {
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
duplicatedEntry: duplicatedRowAlign,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
duplicatedEntry: duplicatedRowBoldAttribute,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
duplicatedEntry: duplicatedRowTextColor,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row insertion attributes: $e');
|
||||
@@ -304,6 +366,18 @@ extension TableMapOperation on Node {
|
||||
index,
|
||||
);
|
||||
|
||||
final (columnBoldAttributes, duplicatedColumnBoldAttribute) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
);
|
||||
|
||||
final (columnTextColors, duplicatedColumnTextColor) =
|
||||
_findDuplicatedEntryAndRemap(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -319,6 +393,16 @@ extension TableMapOperation on Node {
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
duplicatedEntry: duplicatedColumnWidth,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
duplicatedEntry: duplicatedColumnBoldAttribute,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
duplicatedEntry: duplicatedColumnTextColor,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column duplication attributes: $e');
|
||||
@@ -364,6 +448,7 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnAligns = _remapSource(
|
||||
this.columnAligns,
|
||||
index,
|
||||
@@ -371,6 +456,7 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnWidths = _remapSource(
|
||||
this.columnWidths,
|
||||
index,
|
||||
@@ -379,6 +465,22 @@ extension TableMapOperation on Node {
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -391,6 +493,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column deletion attributes: $e');
|
||||
@@ -443,6 +553,22 @@ extension TableMapOperation on Node {
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
index,
|
||||
increment: false,
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -451,6 +577,14 @@ extension TableMapOperation on Node {
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row deletion attributes: $e');
|
||||
@@ -531,6 +665,10 @@ extension TableMapOperation on Node {
|
||||
final duplicatedColumnColor = this.columnColors[fromIndex.toString()];
|
||||
final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()];
|
||||
final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()];
|
||||
final duplicatedColumnBoldAttribute =
|
||||
this.columnBoldAttributes[fromIndex.toString()];
|
||||
final duplicatedColumnTextColor =
|
||||
this.columnTextColors[fromIndex.toString()];
|
||||
|
||||
/// Case 1: fromIndex > toIndex
|
||||
/// Before:
|
||||
@@ -619,6 +757,34 @@ extension TableMapOperation on Node {
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnBoldAttributes = _remapSource(
|
||||
this.columnBoldAttributes,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnTextColors = _remapSource(
|
||||
this.columnTextColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
@@ -652,6 +818,28 @@ extension TableMapOperation on Node {
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnBoldAttributes,
|
||||
columnBoldAttributes,
|
||||
duplicatedEntry: duplicatedColumnBoldAttribute != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnBoldAttribute,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnTextColors,
|
||||
columnTextColors,
|
||||
duplicatedEntry: duplicatedColumnTextColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnTextColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column deletion attributes: $e');
|
||||
@@ -667,6 +855,9 @@ extension TableMapOperation on Node {
|
||||
try {
|
||||
final duplicatedRowColor = this.rowColors[fromIndex.toString()];
|
||||
final duplicatedRowAlign = this.rowAligns[fromIndex.toString()];
|
||||
final duplicatedRowBoldAttribute =
|
||||
this.rowBoldAttributes[fromIndex.toString()];
|
||||
final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()];
|
||||
|
||||
/// Example:
|
||||
/// Case 1: fromIndex > toIndex
|
||||
@@ -742,6 +933,34 @@ extension TableMapOperation on Node {
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final rowBoldAttributes = _remapSource(
|
||||
this.rowBoldAttributes,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final rowTextColors = _remapSource(
|
||||
this.rowTextColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
@@ -764,6 +983,28 @@ extension TableMapOperation on Node {
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowBoldAttributes,
|
||||
rowBoldAttributes,
|
||||
duplicatedEntry: duplicatedRowBoldAttribute != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowBoldAttribute,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowTextColors,
|
||||
rowTextColors,
|
||||
duplicatedEntry: duplicatedRowTextColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowTextColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row reordering attributes: $e');
|
||||
|
||||
+54
@@ -455,6 +455,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final columnIndex = node.columnIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertColumnInTable(table, columnIndex);
|
||||
|
||||
final cell = table.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: columnIndex,
|
||||
);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertColumnRight() {
|
||||
@@ -466,6 +481,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final columnIndex = node.columnIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertColumnInTable(table, columnIndex + 1);
|
||||
|
||||
final cell = table.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: columnIndex + 1,
|
||||
);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertRowAbove() {
|
||||
@@ -477,6 +507,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final rowIndex = node.rowIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertRowInTable(table, rowIndex);
|
||||
|
||||
final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _insertRowBelow() {
|
||||
@@ -488,6 +530,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
final rowIndex = node.rowIndex;
|
||||
final editorState = context.read<EditorState>();
|
||||
editorState.insertRowInTable(table, rowIndex + 1);
|
||||
|
||||
final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0);
|
||||
if (cell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update selection
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(
|
||||
path: cell.path.child(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteRow() {
|
||||
|
||||
@@ -166,9 +166,10 @@ class EditorStyleCustomizer {
|
||||
applyHeightToLastDescent: true,
|
||||
),
|
||||
textSpanDecorator: customizeAttributeDecorator,
|
||||
mobileDragHandleBallSize: const Size.square(12.0),
|
||||
magnifierSize: const Size(144, 96),
|
||||
textScaleFactor: textScaleFactor,
|
||||
mobileDragHandleLeftExtend: 12.0,
|
||||
mobileDragHandleWidthExtend: 24.0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget {
|
||||
|
||||
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
),
|
||||
const HSpace(8.0),
|
||||
_RandomEmojiButton(
|
||||
skinTone: skinTone,
|
||||
emojiData: widget.emojiData,
|
||||
onRandomEmojiSelected: widget.onRandomEmojiSelected,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyEmojiSkinToneSelector(
|
||||
onEmojiSkinToneChanged: widget.onSkinToneChanged,
|
||||
onEmojiSkinToneChanged: (v) {
|
||||
setState(() {
|
||||
skinTone = v;
|
||||
});
|
||||
widget.onSkinToneChanged.call(v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
|
||||
|
||||
class _RandomEmojiButton extends StatelessWidget {
|
||||
const _RandomEmojiButton({
|
||||
required this.skinTone,
|
||||
required this.emojiData,
|
||||
required this.onRandomEmojiSelected,
|
||||
});
|
||||
|
||||
final EmojiSkinTone skinTone;
|
||||
final EmojiData emojiData;
|
||||
final EmojiSelectedCallback onRandomEmojiSelected;
|
||||
|
||||
@@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
final random = emojiData.random;
|
||||
final emojiId = random.$1;
|
||||
final emoji = emojiData.getEmojiById(
|
||||
emojiId,
|
||||
skinTone: skinTone,
|
||||
);
|
||||
onRandomEmojiSelected(
|
||||
random.$1,
|
||||
random.$2,
|
||||
emojiId,
|
||||
emoji,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
|
||||
+53
-11
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart' hide Icon;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
extension ToProto on FlowyIconType {
|
||||
@@ -46,6 +47,10 @@ enum FlowyIconType {
|
||||
custom;
|
||||
}
|
||||
|
||||
extension FlowyIconTypeToPickerTabType on FlowyIconType {
|
||||
PickerTabType? toPickerTabType() => name.toPickerTabType();
|
||||
}
|
||||
|
||||
class EmojiIconData {
|
||||
factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');
|
||||
|
||||
@@ -78,17 +83,35 @@ class EmojiIconData {
|
||||
bool get isNotEmpty => emoji.isNotEmpty;
|
||||
}
|
||||
|
||||
class SelectedEmojiIconResult {
|
||||
SelectedEmojiIconResult(this.data, this.keepOpen);
|
||||
|
||||
final EmojiIconData data;
|
||||
final bool keepOpen;
|
||||
|
||||
FlowyIconType get type => data.type;
|
||||
|
||||
String get emoji => data.emoji;
|
||||
}
|
||||
|
||||
extension EmojiIconDataToSelectedResultExtension on EmojiIconData {
|
||||
SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>
|
||||
SelectedEmojiIconResult(this, keepOpen);
|
||||
}
|
||||
|
||||
class FlowyIconEmojiPicker extends StatefulWidget {
|
||||
const FlowyIconEmojiPicker({
|
||||
super.key,
|
||||
this.onSelectedEmoji,
|
||||
this.initialType,
|
||||
this.enableBackgroundColorSelection = true,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
final ValueChanged<EmojiIconData>? onSelectedEmoji;
|
||||
final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;
|
||||
final bool enableBackgroundColorSelection;
|
||||
final List<PickerTabType> tabs;
|
||||
final PickerTabType? initialType;
|
||||
|
||||
@override
|
||||
State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();
|
||||
@@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget {
|
||||
|
||||
class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final controller = TabController(
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
late TabController controller;
|
||||
int currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialType = widget.initialType;
|
||||
if (initialType != null) {
|
||||
currentIndex = widget.tabs.indexOf(initialType);
|
||||
}
|
||||
controller = TabController(
|
||||
initialIndex: currentIndex,
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
@@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
),
|
||||
_RemoveIconButton(
|
||||
onTap: () {
|
||||
widget.onSelectedEmoji?.call(EmojiIconData.none());
|
||||
widget.onSelectedEmoji
|
||||
?.call(EmojiIconData.none().toSelectedResult());
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
return FlowyEmojiPicker(
|
||||
ensureFocus: true,
|
||||
emojiPerLine: _getEmojiPerLine(context),
|
||||
onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(emoji),
|
||||
),
|
||||
onEmojiSelected: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
|
||||
|
||||
Widget _buildIconPicker() {
|
||||
return FlowyIconPicker(
|
||||
ensureFocus: true,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: (result) {
|
||||
widget.onSelectedEmoji?.call(result.toEmojiIconData());
|
||||
onSelectedIcon: (r) {
|
||||
widget.onSelectedEmoji?.call(
|
||||
r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),
|
||||
);
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,17 +75,31 @@ Future<List<IconGroup>> loadIconGroups() async {
|
||||
}
|
||||
}
|
||||
|
||||
class IconPickerResult {
|
||||
IconPickerResult(this.data, this.isRandom);
|
||||
|
||||
final IconsData data;
|
||||
final bool isRandom;
|
||||
}
|
||||
|
||||
extension IconsDataToIconPickerResultExtension on IconsData {
|
||||
IconPickerResult toResult({bool isRandom = false}) =>
|
||||
IconPickerResult(this, isRandom);
|
||||
}
|
||||
|
||||
class FlowyIconPicker extends StatefulWidget {
|
||||
const FlowyIconPicker({
|
||||
super.key,
|
||||
required this.onSelectedIcon,
|
||||
required this.enableBackgroundColorSelection,
|
||||
this.iconPerLine = 9,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final bool enableBackgroundColorSelection;
|
||||
final ValueChanged<IconsData> onSelectedIcon;
|
||||
final ValueChanged<IconPickerResult> onSelectedIcon;
|
||||
final int iconPerLine;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
|
||||
@@ -142,6 +156,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: IconSearchBar(
|
||||
ensureFocus: widget.ensureFocus,
|
||||
onRandomTap: () {
|
||||
final value = kIconGroups?.randomIcon();
|
||||
if (value == null) {
|
||||
@@ -154,8 +169,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
value.$2.content,
|
||||
value.$2.name,
|
||||
color,
|
||||
),
|
||||
).toResult(isRandom: true),
|
||||
);
|
||||
RecentIcons.putIcon(value.$2);
|
||||
},
|
||||
onKeywordChanged: (keyword) => {
|
||||
debounce.call(() {
|
||||
@@ -193,14 +209,14 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
|
||||
iconGroups: filteredIconGroups,
|
||||
enableBackgroundColorSelection:
|
||||
widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
}
|
||||
return IconPicker(
|
||||
iconGroups: iconGroups,
|
||||
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
|
||||
onSelectedIcon: widget.onSelectedIcon,
|
||||
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
|
||||
iconPerLine: widget.iconPerLine,
|
||||
);
|
||||
},
|
||||
@@ -278,6 +294,7 @@ class _IconPickerState extends State<IconPicker> {
|
||||
crossAxisCount: widget.iconPerLine,
|
||||
),
|
||||
itemCount: iconGroup.icons.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final icon = iconGroup.icons[index];
|
||||
|
||||
@@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget {
|
||||
super.key,
|
||||
required this.onRandomTap,
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final VoidCallback onRandomTap;
|
||||
final bool ensureFocus;
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
|
||||
@override
|
||||
@@ -46,6 +48,7 @@ class _IconSearchBarState extends State<IconSearchBar> {
|
||||
Expanded(
|
||||
child: _SearchTextField(
|
||||
onKeywordChanged: widget.onKeywordChanged,
|
||||
ensureFocus: widget.ensureFocus,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
@@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget {
|
||||
class _SearchTextField extends StatefulWidget {
|
||||
const _SearchTextField({
|
||||
required this.onKeywordChanged,
|
||||
this.ensureFocus = false,
|
||||
});
|
||||
|
||||
final IconKeywordChangedCallback onKeywordChanged;
|
||||
final bool ensureFocus;
|
||||
|
||||
@override
|
||||
State<_SearchTextField> createState() => _SearchTextFieldState();
|
||||
@@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
|
||||
/// this is to ensure that focus can be regained within a short period of time
|
||||
if (widget.ensureFocus) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (!mounted || focusNode.hasFocus) return;
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
@@ -15,6 +15,16 @@ enum PickerTabType {
|
||||
}
|
||||
}
|
||||
|
||||
extension StringToPickerTabType on String {
|
||||
PickerTabType? toPickerTabType() {
|
||||
try {
|
||||
return PickerTabType.values.byName(this);
|
||||
} on ArgumentError {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PickerTab extends StatelessWidget {
|
||||
const PickerTab({
|
||||
super.key,
|
||||
|
||||
@@ -285,14 +285,21 @@ GoRoute _mobileEmojiPickerPageRoute() {
|
||||
state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];
|
||||
final selectTabs =
|
||||
state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';
|
||||
final selectedType = state
|
||||
.uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]
|
||||
?.toPickerTabType();
|
||||
final tabs = selectTabs
|
||||
.split('-')
|
||||
.map((e) => PickerTabType.values.byName(e))
|
||||
.toList();
|
||||
return MaterialExtendedPage(
|
||||
child: tabs.isEmpty
|
||||
? MobileEmojiPickerScreen(title: title)
|
||||
: MobileEmojiPickerScreen(title: title, tabs: tabs),
|
||||
? MobileEmojiPickerScreen(title: title, selectedType: selectedType)
|
||||
: MobileEmojiPickerScreen(
|
||||
title: title,
|
||||
selectedType: selectedType,
|
||||
tabs: tabs,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+30
-11
@@ -18,15 +18,23 @@ enum WorkspaceMoreAction {
|
||||
divider,
|
||||
}
|
||||
|
||||
class WorkspaceMoreActionList extends StatelessWidget {
|
||||
class WorkspaceMoreActionList extends StatefulWidget {
|
||||
const WorkspaceMoreActionList({
|
||||
super.key,
|
||||
required this.workspace,
|
||||
required this.isShowingMoreActions,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final UserWorkspacePB workspace;
|
||||
final ValueNotifier<bool> isShowingMoreActions;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
State<WorkspaceMoreActionList> createState() =>
|
||||
_WorkspaceMoreActionListState();
|
||||
}
|
||||
|
||||
class _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {
|
||||
bool isPopoverOpen = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
||||
return PopoverActionList<_WorkspaceMoreActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
actions: actions
|
||||
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
|
||||
.map(
|
||||
(action) => _WorkspaceMoreActionWrapper(
|
||||
action,
|
||||
widget.workspace,
|
||||
() => PopoverContainer.of(context).closeAll(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
mutex: widget.popoverMutex,
|
||||
constraints: const BoxConstraints(minWidth: 220),
|
||||
animationDuration: Durations.short3,
|
||||
slideDistance: 2,
|
||||
beginScaleFactor: 1.0,
|
||||
beginOpacity: 0.8,
|
||||
onClosed: () {
|
||||
isShowingMoreActions.value = false;
|
||||
},
|
||||
onClosed: () => isPopoverOpen = false,
|
||||
asBarrier: true,
|
||||
buildChild: (controller) {
|
||||
return SizedBox.square(
|
||||
dimension: 24.0,
|
||||
@@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
||||
FlowySvgs.workspace_three_dots_s,
|
||||
),
|
||||
onTap: () {
|
||||
if (!isShowingMoreActions.value) {
|
||||
if (!isPopoverOpen) {
|
||||
controller.show();
|
||||
isPopoverOpen = true;
|
||||
}
|
||||
|
||||
isShowingMoreActions.value = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
||||
_WorkspaceMoreActionWrapper(this.inner, this.workspace);
|
||||
_WorkspaceMoreActionWrapper(
|
||||
this.inner,
|
||||
this.workspace,
|
||||
this.closeWorkspaceMenu,
|
||||
);
|
||||
|
||||
final WorkspaceMoreAction inner;
|
||||
final UserWorkspacePB workspace;
|
||||
final VoidCallback closeWorkspaceMenu;
|
||||
|
||||
@override
|
||||
Widget buildWithContext(
|
||||
@@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
||||
margin: const EdgeInsets.all(6),
|
||||
onTap: () async {
|
||||
PopoverContainer.of(context).closeAll();
|
||||
closeWorkspaceMenu();
|
||||
|
||||
final workspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
switch (inner) {
|
||||
|
||||
+3
-3
@@ -96,9 +96,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
margin: const EdgeInsets.all(0),
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
tabs: const [PickerTabType.emoji],
|
||||
onSelectedEmoji: (result) {
|
||||
widget.onSelected(result);
|
||||
controller.close();
|
||||
onSelectedEmoji: (r) {
|
||||
widget.onSelected(r.data);
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
),
|
||||
child: MouseRegion(
|
||||
|
||||
+58
-52
@@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
final ValueNotifier<bool> isShowingMoreActions = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isShowingMoreActions.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
children: [
|
||||
// user email
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
const _WorkspaceMoreButton(),
|
||||
WorkspaceMoreButton(
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),
|
||||
child: Divider(height: 1.0),
|
||||
),
|
||||
// workspace list
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
userProfile: widget.userProfile,
|
||||
isSelected: workspace.workspaceId ==
|
||||
widget.currentWorkspace.workspaceId,
|
||||
isShowingMoreActions: isShowingMoreActions,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const VSpace(6.0),
|
||||
],
|
||||
@@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
|
||||
),
|
||||
),
|
||||
// add new workspace
|
||||
const _CreateWorkspaceButton(),
|
||||
const VSpace(6.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: _CreateWorkspaceButton(),
|
||||
),
|
||||
|
||||
if (UniversalPlatform.isDesktop) ...[
|
||||
const _ImportNotionButton(),
|
||||
const VSpace(6.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),
|
||||
child: _ImportNotionButton(),
|
||||
),
|
||||
],
|
||||
|
||||
const VSpace(6.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget {
|
||||
required this.workspace,
|
||||
required this.userProfile,
|
||||
required this.isSelected,
|
||||
required this.isShowingMoreActions,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final UserWorkspacePB workspace;
|
||||
final bool isSelected;
|
||||
final ValueNotifier<bool> isShowingMoreActions;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
|
||||
@@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
|
||||
},
|
||||
child: WorkspaceMoreActionList(
|
||||
workspace: widget.workspace,
|
||||
isShowingMoreActions: widget.isShowingMoreActions,
|
||||
popoverMutex: widget.popoverMutex,
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
@@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
FlowyButton(
|
||||
key: importNotionButtonKey,
|
||||
onTap: () {
|
||||
_showImportNotinoDialog(context);
|
||||
child: FlowyButton(
|
||||
key: importNotionButtonKey,
|
||||
onTap: () {
|
||||
_showImportNotinoDialog(context);
|
||||
},
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
text: Row(
|
||||
children: [
|
||||
_buildLeftIcon(context),
|
||||
const HSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.workspace_importFromNotion.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
rightIcon: FlowyTooltip(
|
||||
message: LocaleKeys.workspace_learnMore.tr(),
|
||||
preferBelow: true,
|
||||
child: FlowyIconButton(
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.information_s,
|
||||
),
|
||||
onPressed: () {
|
||||
afLaunchUrlString(
|
||||
'https://docs.appflowy.io/docs/guides/import-from-notion',
|
||||
);
|
||||
},
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
text: Row(
|
||||
children: [
|
||||
_buildLeftIcon(context),
|
||||
const HSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.workspace_importFromNotion.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.workspace_learnMore.tr(),
|
||||
preferBelow: true,
|
||||
child: FlowyIconButton(
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.information_s,
|
||||
),
|
||||
onPressed: () {
|
||||
afLaunchUrlString(
|
||||
'https://docs.appflowy.io/docs/guides/import-from-notion',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkspaceMoreButton extends StatelessWidget {
|
||||
const _WorkspaceMoreButton();
|
||||
@visibleForTesting
|
||||
class WorkspaceMoreButton extends StatelessWidget {
|
||||
const WorkspaceMoreButton({
|
||||
super.key,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 6),
|
||||
mutex: popoverMutex,
|
||||
asBarrier: true,
|
||||
popupBuilder: (_) => FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
|
||||
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),
|
||||
|
||||
+1
@@ -207,6 +207,7 @@ class _SidebarSwitchWorkspaceButtonState
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 5),
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||
margin: EdgeInsets.zero,
|
||||
animationDuration: Durations.short3,
|
||||
beginScaleFactor: 1.0,
|
||||
beginOpacity: 0.8,
|
||||
|
||||
@@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
popupBuilder: (context) {
|
||||
isIconPickerOpened = true;
|
||||
return FlowyIconEmojiPicker(
|
||||
onSelectedEmoji: (result) {
|
||||
initialType: iconData.type.toPickerTabType(),
|
||||
onSelectedEmoji: (r) {
|
||||
ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
controller.close();
|
||||
if (!r.keepOpen) controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
||||
break;
|
||||
case ViewMoreActionType.changeIcon:
|
||||
if (data is! EmojiIconData) {
|
||||
if (data is! SelectedEmojiIconResult) {
|
||||
return;
|
||||
}
|
||||
final result = data;
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result,
|
||||
viewIcon: data.data,
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.moveTo:
|
||||
|
||||
+6
-1
@@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget {
|
||||
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||
onEditing(false);
|
||||
onAction(e, data);
|
||||
controller.close();
|
||||
bool enableClose = true;
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.keepOpen) enableClose = false;
|
||||
}
|
||||
if (enableClose) controller.close();
|
||||
}),
|
||||
)
|
||||
.toList();
|
||||
@@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
margin: const EdgeInsets.all(0),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (_) => FlowyIconEmojiPicker(
|
||||
initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),
|
||||
onSelectedEmoji: (result) => onTap(controller, result),
|
||||
),
|
||||
child: child,
|
||||
|
||||
+4
-6
@@ -1,4 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
@@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
final icon = publishInfoView.view.icon.value;
|
||||
final icon = publishInfoView.view.icon.toEmojiIconData();
|
||||
return icon.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 18.0,
|
||||
)
|
||||
? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0)
|
||||
: publishInfoView.view.defaultIcon();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,12 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
|
||||
getName: LocaleKeys.document_plugins_emoji.tr,
|
||||
@@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyEmojiPicker(
|
||||
onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji),
|
||||
onEmojiSelected: (r) => widget.onSubmitted(r.emoji),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
@@ -364,71 +365,59 @@ class OkCancelButton extends StatelessWidget {
|
||||
|
||||
void showToastNotification(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
String? message,
|
||||
TextSpan? richMessage,
|
||||
String? description,
|
||||
ToastificationType type = ToastificationType.success,
|
||||
ToastificationCallbacks? callbacks,
|
||||
double bottomPadding = 100,
|
||||
}) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
toastification.showCustom(
|
||||
alignment: Alignment.bottomCenter,
|
||||
autoCloseDuration: const Duration(milliseconds: 3000),
|
||||
callbacks: callbacks ?? const ToastificationCallbacks(),
|
||||
builder: (_, __) => _MToast(
|
||||
message: message,
|
||||
type: type,
|
||||
bottomPadding: bottomPadding,
|
||||
description: description,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toastification.show(
|
||||
context: context,
|
||||
type: type,
|
||||
style: ToastificationStyle.flat,
|
||||
closeButtonShowType: CloseButtonShowType.onHover,
|
||||
assert(
|
||||
(message == null) != (richMessage == null),
|
||||
"Exactly one of message or richMessage must be non-null.",
|
||||
);
|
||||
toastification.showCustom(
|
||||
alignment: Alignment.bottomCenter,
|
||||
autoCloseDuration: const Duration(milliseconds: 3000),
|
||||
showProgressBar: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
),
|
||||
title: FlowyText(
|
||||
message,
|
||||
maxLines: 3,
|
||||
),
|
||||
description: description != null
|
||||
? FlowyText.regular(
|
||||
description,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
maxLines: 3,
|
||||
)
|
||||
: null,
|
||||
callbacks: callbacks ?? const ToastificationCallbacks(),
|
||||
builder: (_, item) {
|
||||
return UniversalPlatform.isMobile
|
||||
? _MobileToast(
|
||||
message: message,
|
||||
type: type,
|
||||
bottomPadding: bottomPadding,
|
||||
description: description,
|
||||
)
|
||||
: _DesktopToast(
|
||||
message: message,
|
||||
richMessage: richMessage,
|
||||
type: type,
|
||||
onDismiss: () => toastification.dismiss(item),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _MToast extends StatelessWidget {
|
||||
const _MToast({
|
||||
required this.message,
|
||||
class _MobileToast extends StatelessWidget {
|
||||
const _MobileToast({
|
||||
this.message,
|
||||
this.type = ToastificationType.success,
|
||||
this.bottomPadding = 100,
|
||||
this.description,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final String? message;
|
||||
final ToastificationType type;
|
||||
final double bottomPadding;
|
||||
final String? description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final hintText = FlowyText.regular(
|
||||
message,
|
||||
message!,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 18.0,
|
||||
color: Colors.white,
|
||||
@@ -498,6 +487,90 @@ class _MToast extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopToast extends StatelessWidget {
|
||||
const _DesktopToast({
|
||||
this.message,
|
||||
this.richMessage,
|
||||
required this.type,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
final String? message;
|
||||
final TextSpan? richMessage;
|
||||
final ToastificationType type;
|
||||
final void Function()? onDismiss;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 360.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
margin: const EdgeInsets.only(bottom: 32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).isLightMode
|
||||
? const Color(0xFF333333)
|
||||
: const Color(0xFF363D49),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// icon
|
||||
FlowySvg(
|
||||
switch (type) {
|
||||
ToastificationType.warning => FlowySvgs.toast_warning_filled_s,
|
||||
ToastificationType.success => FlowySvgs.toast_checked_filled_s,
|
||||
ToastificationType.error => FlowySvgs.toast_error_filled_s,
|
||||
_ => throw UnimplementedError(),
|
||||
},
|
||||
size: const Size.square(20.0),
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
// text
|
||||
Flexible(
|
||||
child: message != null
|
||||
? FlowyText(
|
||||
message!,
|
||||
maxLines: 2,
|
||||
figmaLineHeight: 20.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: const Color(0xFFFFFFFF),
|
||||
)
|
||||
: RichText(
|
||||
text: richMessage!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(16.0),
|
||||
// close
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: const SizedBox.square(
|
||||
dimension: 24.0,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(16.0),
|
||||
color: Color(0xFFBDBDBD),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showConfirmDeletionDialog({
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
|
||||
+5
-3
@@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
|
||||
}
|
||||
|
||||
Future<void> _updateViewIcon(
|
||||
EmojiIconData emoji,
|
||||
SelectedEmojiIconResult r,
|
||||
PopoverController? _,
|
||||
) async {
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.viewId,
|
||||
viewIcon: emoji,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
widget.popoverController.close();
|
||||
if (!r.keepOpen) {
|
||||
widget.popoverController.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: c68e5f6
|
||||
resolved-ref: c68e5f6c585205083e27e875b822656425b2853f
|
||||
ref: cfb8b1b
|
||||
resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
@@ -757,8 +757,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "8a9fa49"
|
||||
resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028"
|
||||
ref: "355aa56"
|
||||
resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8"
|
||||
url: "https://github.com/LucasXu0/emoji_mart.git"
|
||||
source: git
|
||||
version: "1.0.2"
|
||||
|
||||
@@ -73,7 +73,7 @@ dependencies:
|
||||
flutter_emoji_mart:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/emoji_mart.git
|
||||
ref: "8a9fa49"
|
||||
ref: "355aa56"
|
||||
flutter_math_fork: ^0.7.3
|
||||
flutter_slidable: ^3.0.0
|
||||
|
||||
@@ -174,7 +174,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "c68e5f6"
|
||||
ref: "cfb8b1b"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
||||
+62
@@ -170,5 +170,67 @@ void main() {
|
||||
'0': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('delete a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// delete the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.deleteColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.columnLength, 3);
|
||||
});
|
||||
|
||||
test('delete a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// delete the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.deleteColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {});
|
||||
expect(tableNode.columnBoldAttributes, {});
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.columnLength, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
+64
@@ -161,5 +161,69 @@ void main() {
|
||||
'1': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('duplicate a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// duplicate the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.duplicateColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
'2': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
'2': true,
|
||||
});
|
||||
});
|
||||
|
||||
test('duplicate a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 4,
|
||||
);
|
||||
// duplicate the column 1
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
await editorState.duplicateColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'2': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'2': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+62
@@ -190,5 +190,67 @@ void main() {
|
||||
'0': TableAlign.center.key,
|
||||
});
|
||||
});
|
||||
|
||||
test('insert a column with text color & bold style (1)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
);
|
||||
// insert the column at the first position
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
await editorState.insertColumnInTable(tableNode, 0);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'1': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'1': true,
|
||||
});
|
||||
});
|
||||
|
||||
test('insert a column with text color & bold style (2)', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
);
|
||||
// insert the column at the first position
|
||||
final tableCellNode =
|
||||
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
|
||||
await editorState.updateColumnTextColor(
|
||||
tableCellNode: tableCellNode!,
|
||||
color: '0xFF0000FF',
|
||||
);
|
||||
await editorState.toggleColumnBoldAttribute(
|
||||
tableCellNode: tableCellNode,
|
||||
isBold: true,
|
||||
);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
await editorState.insertColumnInTable(tableNode, 1);
|
||||
expect(tableNode.columnTextColors, {
|
||||
'0': '0xFF0000FF',
|
||||
});
|
||||
expect(tableNode.columnBoldAttributes, {
|
||||
'0': true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6 13.8L8.45 11.65C8.26667 11.4667 8.03333 11.375 7.75 11.375C7.46667 11.375 7.23333 11.4667 7.05 11.65C6.86667 11.8333 6.775 12.0667 6.775 12.35C6.775 12.6333 6.86667 12.8667 7.05 13.05L9.9 15.9C10.1 16.1 10.3333 16.2 10.6 16.2C10.8667 16.2 11.1 16.1 11.3 15.9L16.95 10.25C17.1333 10.0667 17.225 9.83333 17.225 9.55C17.225 9.26667 17.1333 9.03333 16.95 8.85C16.7667 8.66667 16.5333 8.575 16.25 8.575C15.9667 8.575 15.7333 8.66667 15.55 8.85L10.6 13.8ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#3AC25C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16C13 15.7167 12.9042 15.4792 12.7125 15.2875C12.5208 15.0958 12.2833 15 12 15C11.7167 15 11.4792 15.0958 11.2875 15.2875C11.0958 15.4792 11 15.7167 11 16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 13C12.2833 13 12.5208 12.9042 12.7125 12.7125C12.9042 12.5208 13 12.2833 13 12V8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8V12C11 12.2833 11.0958 12.5208 11.2875 12.7125C11.4792 12.9042 11.7167 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#FB006D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.72503 21C2.5417 21 2.37503 20.9542 2.22503 20.8625C2.07503 20.7708 1.95837 20.65 1.87503 20.5C1.7917 20.35 1.74587 20.1875 1.73753 20.0125C1.7292 19.8375 1.77503 19.6667 1.87503 19.5L11.125 3.5C11.225 3.33333 11.3542 3.20833 11.5125 3.125C11.6709 3.04167 11.8334 3 12 3C12.1667 3 12.3292 3.04167 12.4875 3.125C12.6459 3.20833 12.775 3.33333 12.875 3.5L22.125 19.5C22.225 19.6667 22.2709 19.8375 22.2625 20.0125C22.2542 20.1875 22.2084 20.35 22.125 20.5C22.0417 20.65 21.925 20.7708 21.775 20.8625C21.625 20.9542 21.4584 21 21.275 21H2.72503ZM12 18C12.2834 18 12.5209 17.9042 12.7125 17.7125C12.9042 17.5208 13 17.2833 13 17C13 16.7167 12.9042 16.4792 12.7125 16.2875C12.5209 16.0958 12.2834 16 12 16C11.7167 16 11.4792 16.0958 11.2875 16.2875C11.0959 16.4792 11 16.7167 11 17C11 17.2833 11.0959 17.5208 11.2875 17.7125C11.4792 17.9042 11.7167 18 12 18ZM12 15C12.2834 15 12.5209 14.9042 12.7125 14.7125C12.9042 14.5208 13 14.2833 13 14V11C13 10.7167 12.9042 10.4792 12.7125 10.2875C12.5209 10.0958 12.2834 10 12 10C11.7167 10 11.4792 10.0958 11.2875 10.2875C11.0959 10.4792 11 10.7167 11 11V14C11 14.2833 11.0959 14.5208 11.2875 14.7125C11.4792 14.9042 11.7167 15 12 15Z" fill="#FF7E1E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.3327 8.12533C15.3327 12.1754 12.0494 15.4587 7.99935 15.4587C3.94926 15.4587 0.666016 12.1754 0.666016 8.12533C0.666016 4.07524 3.94926 0.791992 7.99935 0.791992C12.0494 0.791992 15.3327 4.07524 15.3327 8.12533Z" fill="#FB006D"/>
|
||||
<path d="M8.00065 4.79199C7.63246 4.79199 7.33398 5.09047 7.33398 5.45866V8.79199C7.33398 9.16018 7.63246 9.45866 8.00065 9.45866C8.36884 9.45866 8.66732 9.16018 8.66732 8.79199V5.45866C8.66732 5.09047 8.36884 4.79199 8.00065 4.79199Z" fill="white"/>
|
||||
<path d="M8.00065 10.1253C7.63246 10.1253 7.33398 10.4238 7.33398 10.792C7.33398 11.1602 7.63246 11.4587 8.00065 11.4587C8.36884 11.4587 8.66732 11.1602 8.66732 10.792C8.66732 10.4238 8.36884 10.1253 8.00065 10.1253Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 830 B |
@@ -218,7 +218,9 @@
|
||||
"addToPageButton": "Add to page",
|
||||
"addToPageTitle": "Add message to...",
|
||||
"addToNewPage": "Add to a new page",
|
||||
"addToNewPageName": "Messages extracted from \"{}\""
|
||||
"addToNewPageName": "Messages extracted from \"{}\"",
|
||||
"addToNewPageSuccessToast": "Message added to",
|
||||
"openPagePreviewFailedToast": "Failed to open page"
|
||||
},
|
||||
"trash": {
|
||||
"text": "Trash",
|
||||
|
||||
Reference in new issue
Block a user