Compare commits

...
42 Commits
Author SHA1 Message Date
Mathias Mogensen b2978e0d6c fix: launch review 0.5.8 (#5367) 2024-05-21 11:34:36 +02:00
Kilu.He b7bc847107 feat: support board preview on web (#5384) 2024-05-21 17:26:00 +08:00
Lucas.Xu aa07393253 fix: unable to remove the remote selection when the user close the page on mobile (#5376)
* fix: unable to remove the remote selection when the user close the page on mobile

* fix: unable to apply settings font to editor

* chore: remove transparent background color
2024-05-21 15:07:15 +08:00
Nathan.fooo e85dbe724c chore: add limitation when importing csv file (#5381) 2024-05-21 14:41:47 +08:00
Lucas.Xu 24b3d69860 fix: infinite loop when getting ancestor for orphan view (#5375) 2024-05-21 10:47:31 +08:00
Lucas.Xu c67e266174 fix: range error raise if heading level >= 6 (#5373) 2024-05-21 09:21:50 +08:00
Lucas.Xu d303d5affd fix: use full update states instead of patch update (#5371) 2024-05-20 21:51:40 +08:00
Stefan Weiberg e6d166df3b chore: update translations to consistently use appName instead of AppFlowy (#5354) 2024-05-20 17:20:14 +08:00
Mohammad Zolfaghari 9b7ee4b978 fix: changing field type from text to checkbox causes exception #5360 (#5366) 2024-05-20 17:19:58 +08:00
Lucas.Xu 0c0bd54f52 fix: heading node validate failed (#5370) 2024-05-20 17:18:39 +08:00
Vladislav Poturai f1b035e362 chore: support Ukrainian language #5350 (#5369) 2024-05-20 11:28:13 +08:00
Nathan.fooo bd62a161be chore: do not use the cell of given summary field (#5362)
* chore: do not use the cell of given summary field

* chore: update docs
2024-05-19 21:35:14 +08:00
atellier2 7e83d598f8 chore: update translations with Fink 🐦 (#5322) 2024-05-18 15:57:56 +02:00
JB bfc6cd721e chore: update fr-FR.json (#5356)
Fix typo
2024-05-18 15:56:26 +02:00
Lucas.Xu 3c6ce4ea4d fix: workspace icon displays incorrectly on Linux (#5358) 2024-05-17 23:15:37 +08:00
Kilu.He c1e7e72154 feat: support web grid preview (#5353) 2024-05-17 18:23:29 +08:00
Nathan.fooo 759998cacb chore: enable ai summary (#5351) 2024-05-17 10:22:05 +08:00
Stefan Weiberg a6467c1b16 chore: update German translations with Fink 🐦 (#5335) 2024-05-16 17:31:02 +08:00
Mathias MogensenandLucas.Xu edfd1ab116 chore: bump version 0.5.8 (#5341)
* chore: bump version 0.5.8

* chore: update release date

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* chore: reverse order

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-05-16 08:09:02 +02:00
Sander Hoeve c3c99c7960 fix: add space above logo (#5334) 2024-05-15 22:42:18 +02:00
Lucas.Xu 3bead0bab9 feat: adjust font background color in dark mode (#5333) 2024-05-14 15:36:23 +08:00
Lucas.Xu eb0c9d3264 feat: support enter to insert new line in callout (#5331) 2024-05-14 15:35:01 +08:00
Stefan Weiberg 5c7ba0466f chore: update translations with Fink 🐦 (#5321) 2024-05-14 13:46:17 +08:00
Lucas.Xu b1177ad3ea fix: font family warning (#5329) 2024-05-14 13:44:51 +08:00
Mathias Mogensen b9faf3b24a fix: show windows title bar on sign in screen (#5326) 2024-05-13 16:14:36 +02:00
Mathias Mogensen 12ff548b09 fix: view action not working (#5325) 2024-05-13 15:45:27 +02:00
Richard Shiue 027ab2c1ef fix: text update bug on number/url fields (#5315) 2024-05-13 10:30:17 +02:00
Mathias Mogensen 8273d66c50 feat: settings manage data (#5265)
* feat: settings manage data page

* fix: changes after merge

* test: fix failing integration test

* fix: missing localizations
2024-05-13 09:45:56 +02:00
Richard Shiue 38fa9f7942 chore: improve floating mobile toolbar animation for iOS (#5309) 2024-05-13 14:58:28 +08:00
Bartosz Sypytkowski 39f8c47758 chore: udpated yrs to v0.18.8 (#5316) 2024-05-13 14:58:10 +08:00
Lucas.Xu 4cfd83cbc4 fix: page style issues (#5317)
* fix: 7 emojis per line

* fix: remove shadow of the icons using presets cover

* fix: expand appbar buttons size

* fix: save new title name when it changed

* feat: add cover image preview

* fix: dismiss unsplash and presets panel auto

* feat: add selected color for cover image and layout section

* fix: selected icon size too small

* fix: dismiss page style panel before pushing to emoji and font selector

* chore: update back button icon

* chore: bump version 0.5.7
2024-05-13 13:26:19 +08:00
Mathias Mogensen cdcb393efd feat: custom windows title bar (#5311) 2024-05-12 22:09:55 +02:00
Mathias MogensenandLucas.Xu a0ed043cb8 feat: workspace settings page (#5225)
* feat: my account settings page

* test: amend tests

* chore: remove unused code

* test: remove widget tests

* fix: text color on select buttons

* test: clean and remove unused test helpers

* feat: settings workspace page

* chore: fixes after merge

* fix: recent views bugfix

* fix: make sure text buttons have color

* test: add test for delete workspace in settings

* test: remove pumpAndSettle for create workspace

* test: longer pump duration

* test: attempt with large pump duration

* test: attempt workaround

* chore: clean code

* fix: missing language key

* test: add one more check

* test: pump

* test: more pump

* test: attempt pumpAndSettle

* chore: code review

* fix: persist single workspace on patch

* fix: listen to workspace changes

* chore: remove redundant builder

* test: remove unstable test

* fix: changes after merge

* chore: changes after merge

* feat: support changing cursor and selection color

* chore: move members up in menu

* feat: clean code and beautify dialogs

* fix: fix test and make show selected font

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-05-10 16:08:32 +02:00
Richard Shiue f47c88b022 fix: kanban board card text input inconsistency (#5307) 2024-05-10 11:25:22 +08:00
Lucas.Xu c732438f8a feat: support scale up/down the app (#5300) 2024-05-10 09:09:25 +07:00
Lucas.Xu f3b8ec8a04 fix: android navigation bar issues (#5294) 2024-05-10 09:09:11 +07:00
Richard ShiueandMathias Mogensen a490f34a61 feat: add kanban shortcuts (#5270)
* feat: add kanban shortcuts

* feat: new ux for creating new kanban cards

* chore: fix tests

* fix: open card after creation in mobile board

* chore: adjust code style according to launch review

* chore: update frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: more review

* chore: implement move card to adjacent group

* chore: reset focus upon card drag start

* feat: N to start creating a row from bottom

* fix: text card update

* feat: shift + enter to create a new card after currently focused card

* fix: row detail title

* feat: shift + cmd + up to create card above

* fix: double dispose and code cleanup

* chore: code cleanup

* fix: widget rebuilds

* fix: build

* chore: update frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* fix: ontapoutside for cards being edited

* fix: correct integration test

* fix: always build

* chore: code cleanup

* fix: mobile build and bugs

* fix: widget rebuilds

* fix: code cleanup and fix mobile open

* fix: disallow dragging when editing

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-05-10 10:02:10 +08:00
Nathan.fooo 28a27d1b67 chore: use patch to fix version issue (#5291) 2024-05-10 07:56:10 +08:00
Lucas.Xu 6edb184bfb refactor: mobile view page (#5288)
* refactor: mobile view page

* fix: provider not found

* chore: add page style integration tests

* fix: android title bar padding

* fix: unable to click mentioned page on Android

* fix: font family not available log
2024-05-09 13:32:35 +08:00
Richard Shiue dbbdc13d96 fix: database row page breadcrumbs on smaller screens (#5289) 2024-05-08 22:05:50 +08:00
TyrantReyandLucas.Xu eeddf341e1 chore: translate english to traditional chinese (#5278)
* chore: update translations with Fink 🐦

* chore: remove unused translations

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-05-08 10:37:48 +08:00
DekotaleandLucas.Xu aaa72fc143 chore: update translations with Fink 🐦 (#5249)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-05-08 10:37:36 +08:00
501 changed files with 16263 additions and 5683 deletions

No files matched your search

+13 -4
View File
@@ -88,7 +88,16 @@ jobs:
model: 'iPhone 15'
shutdown_after_job: false
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
# run: |
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
# pid=$!
# sleep 500
# kill $pid
# continue-on-error: true
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+35
View File
@@ -1,4 +1,39 @@
# Release Notes
## Version 0.5.8 - 05/20/2024
### New Features
- Improvement to the Callout block to insert new lines
- New settings page "Manage data" replaced the "Files" page
- New settings page "Workspace" replaced the "Appearance" and "Language" pages
- A custom implementation of a title bar for Windows users
- Added support for selecting Cards in kanban and performing grouped keyboard shortcuts
- Added support for default system font family
- Support for scaling the application up/down using a keyboard shortcut (CMD/CTRL + PLUS/MINUS)
### Bug Fixes
- Resolved and refined the UI on Mobile
- Resolved issue with text editing in database
- Improved appearance of empty text cells in kanban/calendar
- Resolved an issue where a page's more actions (delete, duplicate) did not work properly
- Resolved and inconsistency in padding on get started screen on Desktop
## Version 0.5.7 - 05/10/2024
### Bug Fixes
- Resolved page opening issue on Android.
- Fixed text input inconsistency on Kanban board cards.
## Version 0.5.6 - 05/07/2024
### New Features
- Team collaboration is live! Add members to your workspace to edit and collaborate on pages together.
- Collaborate in real time on the same page with other members. Edits made by others will appear instantly.
- Create multiple workspaces for different kinds of content.
- Customize your entire page on mobile through the Page Style menu with options for layout, font, font size, emoji, and cover image.
- Open a row record as a full page.
### Bug Fixes
- Resolved issue with setting background color for the Simple Table block.
- Adjusted toolbar for various screen sizes.
- Added a request for photo permission before uploading images on mobile.
- Exported creation and last modification timestamps to CSV.
## Version 0.5.5 - 04/24/2024
### New Features
- Improved the display of code blocks with line numbers
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.5.6"
APPFLOWY_VERSION = "0.5.8"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -60,9 +60,12 @@ void main() {
Finder success;
final Finder items = find.byType(WorkspaceMenuItem);
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
final Finder items = find.byType(WorkspaceMenuItem);
await tester.pumpUntilFound(items);
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -82,23 +83,19 @@ void main() {
findsOneWidget,
);
await tester.tap(
find
.descendant(
of: find.byType(AppFlowyGroupFooter),
matching: find.byType(FlowySvg),
)
.at(1),
await tester.tapButton(
find.byType(BoardColumnFooter).at(1),
);
const newCardName = 'Card 4';
await tester.enterText(
find.descendant(
of: lastCard,
of: find.byType(BoardColumnFooter),
matching: find.byType(TextField),
),
newCardName,
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.byType(AppFlowyBoard));
@@ -0,0 +1,37 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board field test', () {
testWidgets('change field type whithin card #5360', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.text(name);
await tester.tapButton(card1);
const fieldName = "test change field";
await tester.createField(
FieldType.RichText,
fieldName,
layout: ViewLayoutPB.Board,
);
await tester.tapButton(card1);
await tester.changeFieldTypeOfFieldWithName(
fieldName,
FieldType.Checkbox,
layout: ViewLayoutPB.Board,
);
await tester.hoverOnWidget(find.text('Card 2'));
});
});
}
@@ -0,0 +1,32 @@
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('MoreViewActions', () {
testWidgets('can duplicate and delete from menu', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.pumpAndSettle();
final pageFinder = find.byType(ViewItem);
expect(pageFinder, findsNWidgets(1));
// Duplicate
await tester.openMoreViewActions();
await tester.duplicateByMoreViewActions();
expect(pageFinder, findsNWidgets(2));
// Delete
await tester.openMoreViewActions();
await tester.deleteByMoreViewActions();
expect(pageFinder, findsNWidgets(1));
});
});
}
@@ -6,6 +6,9 @@ import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
@@ -16,8 +19,6 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -38,4 +39,5 @@ void startTesting() {
document_option_action_test.main();
document_with_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
}
@@ -56,7 +56,7 @@ void main() {
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(BoardPage),
matching: find.byType(DesktopBoardPage),
),
findsOneWidget,
);
@@ -104,7 +104,7 @@ void main() {
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(BoardPage),
matching: find.byType(DesktopBoardPage),
),
findsOneWidget,
);
@@ -1,11 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'user_language_test.dart' as user_language_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
notifications_settings_test.main();
user_language_test.main();
}
@@ -1,70 +0,0 @@
import 'dart:ui';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings: user language tests', () {
testWidgets('select language, language changed', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.language);
final userLanguageFinder = find.descendant(
of: find.byType(SettingsLanguageView),
matching: find.byType(LanguageSelector),
);
// Grab current locale
LanguageSelector userLanguage =
tester.widget<LanguageSelector>(userLanguageFinder);
Locale currentLocale = userLanguage.currentLocale;
// Open language selector
await tester.tap(userLanguageFinder);
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(LanguageItem).at(1));
await tester.pumpAndSettle();
// Make sure the new locale is not the same as previous one
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
expect(
userLanguage.currentLocale,
isNot(equals(currentLocale)),
reason: "new language shouldn't equal the previous selected language",
);
// Update the current locale to a new one
currentLocale = userLanguage.currentLocale;
// Tried the same flow for the second time
// Open language selector
await tester.tap(userLanguageFinder);
await tester.pumpAndSettle();
// Select second option that isn't default
await tester.tap(find.byType(LanguageItem).at(2));
await tester.pumpAndSettle();
// Make sure the new locale is not the same as previous one
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
expect(
userLanguage.currentLocale,
isNot(equals(currentLocale)),
reason: "new language shouldn't equal the previous selected language",
);
});
});
}
@@ -61,7 +61,7 @@ void main() {
expect(find.byType(GridPage), findsOneWidget);
break;
case ViewLayoutPB.Board:
expect(find.byType(BoardPage), findsOneWidget);
expect(find.byType(DesktopBoardPage), findsOneWidget);
break;
case ViewLayoutPB.Calendar:
expect(find.byType(CalendarPage), findsOneWidget);
@@ -1,93 +0,0 @@
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appearance settings tests', () {
testWidgets('after editing text field, button should be able to be clicked',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
await tester.tap(dropDown);
await tester.pumpAndSettle();
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
await tester.tap(textField);
await tester.pumpAndSettle();
await tester.enterText(textField, 'Abel');
await tester.pumpAndSettle();
final fontFamilyButton = find.byKey(const Key('Abel'));
expect(fontFamilyButton, findsOneWidget);
await tester.tap(fontFamilyButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(find.textContaining('Abel'), findsOneWidget);
});
testWidgets('reset the font family', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
await tester.tap(dropDown);
await tester.pumpAndSettle();
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
await tester.tap(textField);
await tester.pumpAndSettle();
await tester.enterText(textField, 'Abel');
await tester.pumpAndSettle();
final fontFamilyButton = find.byKey(const Key('Abel'));
expect(fontFamilyButton, findsOneWidget);
await tester.tap(fontFamilyButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
final resetButton = find.byKey(ThemeFontFamilySetting.resetButtonKey);
await tester.tap(resetButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(
find.textContaining(
DefaultAppearanceSettings.kDefaultFontFamily.fontFamilyDisplayName,
),
findsNWidgets(2),
);
});
});
}
@@ -1,11 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.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';
@@ -23,31 +25,35 @@ void main() {
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
await tester.openSettingsPage(SettingsPage.workspace);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_system.tr(),
);
final appFinder = find.byType(MaterialApp).first;
ThemeMode? themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.system);
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_system.tr(),
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
),
);
await tester.pumpAndSettle();
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_dark.tr(),
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
),
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 1));
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.dark);
await tester.tap(find.byType(SettingsDialog));
await tester.pumpAndSettle();
await FlowyTestKeyboard.simulateKeyDownEvent(
@@ -60,12 +66,10 @@ void main() {
],
tester: tester,
);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_light.tr(),
);
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
});
testWidgets('show or hide home menu', (tester) async {
@@ -1,4 +1,4 @@
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/user/presentation/screens/skip_log_in_screen.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -101,7 +101,7 @@ void main() {
// open settings and restore the location
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.manageData);
await tester.restoreLocation();
expect(
@@ -3,8 +3,6 @@ import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/appearance_settings_test.dart'
as appearance_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
@@ -26,7 +24,6 @@ Future<void> runIntegration3OnDesktop() async {
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
appearance_test_runner.main();
settings_test_runner.main();
share_markdown_test.main();
import_files_test.main();
@@ -0,0 +1,139 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document page style', () {
double getCurrentEditorFontSize() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.fontSize!;
}
double getCurrentEditorLineHeight() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.height!;
}
testWidgets('change font size in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize);
// change font size from normal to large
await tester.tapSvgButton(FlowySvgs.m_font_size_large_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize);
// change font size from large to small
await tester.tapSvgButton(FlowySvgs.m_font_size_small_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize);
});
testWidgets('change line height in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.normal.lineHeight,
);
// change line height from normal to large
await tester.tapSvgButton(FlowySvgs.m_layout_large_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.large.lineHeight,
);
// change line height from large to small
await tester.tapSvgButton(FlowySvgs.m_layout_small_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.small.lineHeight,
);
});
testWidgets('use built-in image as cover', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m);
// select the first preset
final firstBuiltInImage = find.byWidgetPredicate(
(widget) =>
widget is Image &&
widget.image is AssetImage &&
(widget.image as AssetImage).assetName ==
PageStyleCoverImageType.builtInImagePath('1'),
);
await tester.tap(firstBuiltInImage);
// click done button to exit the page style settings
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
// check the cover
final builtInCover = find.descendant(
of: find.byType(DocumentImmersiveCover),
matching: firstBuiltInImage,
);
expect(builtInCover, findsOneWidget);
});
});
}
@@ -1,8 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
}
@@ -173,27 +173,39 @@ extension AppFlowyTestBase on WidgetTester {
int buttons = kPrimaryButton,
bool warnIfMissed = false,
int milliseconds = 500,
bool pumpAndSettle = true,
}) async {
await tap(
finder,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
);
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
);
}
}
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
bool pumpAndSettle = true,
}) async {
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(button, milliseconds: milliseconds);
await tapButton(
button,
milliseconds: milliseconds,
pumpAndSettle: pumpAndSettle,
);
}
Future<void> doubleTapAt(
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -8,6 +9,7 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
@@ -25,7 +27,8 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -510,8 +513,9 @@ extension CommonOperations on WidgetTester {
final workspace = find.byType(SidebarWorkspace);
expect(workspace, findsOneWidget);
// click it
await tapButton(workspace, milliseconds: 2000);
await tapButton(workspace, pumpAndSettle: false);
await pump(const Duration(seconds: 5));
}
Future<void> createCollaborativeWorkspace(String name) async {
@@ -526,7 +530,8 @@ extension CommonOperations on WidgetTester {
// click the create button
final createButton = find.byKey(createWorkspaceButtonKey);
expect(createButton, findsOneWidget);
await tapButton(createButton);
await tapButton(createButton, pumpAndSettle: false);
await pump(const Duration(seconds: 5));
// see the create workspace dialog
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
@@ -535,7 +540,70 @@ extension CommonOperations on WidgetTester {
// input the workspace name
await enterText(find.byType(TextField), name);
await tapButtonWithName(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
await pump(const Duration(seconds: 5));
}
// For mobile platform to launch the app in anonymous mode
Future<void> launchInAnonymousMode() async {
assert(
[TargetPlatform.android, TargetPlatform.iOS]
.contains(defaultTargetPlatform),
'This method is only supported on mobile platforms',
);
await initializeAppFlowy();
final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);
expect(anonymousSignInButton, findsOneWidget);
await tapButton(anonymousSignInButton);
await pumpUntilFound(find.byType(MobileHomeScreen));
}
Future<void> tapSvgButton(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
);
await tapButton(button);
}
Future<void> openMoreViewActions() async {
final button = find.byType(MoreViewActions);
await tap(button);
await pumpAndSettle();
}
/// Presses on the Duplicate ViewAction in the [MoreViewActions] popup.
///
/// [openMoreViewActions] must be called beforehand!
///
Future<void> duplicateByMoreViewActions() async {
final button = find.descendant(
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewActionType.duplicate,
),
);
await tap(button);
await pump();
}
/// Presses on the Delete ViewAction in the [MoreViewActions] popup.
///
/// [openMoreViewActions] must be called beforehand!
///
Future<void> deleteByMoreViewActions() async {
final button = find.descendant(
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewActionType.delete,
),
);
await tap(button);
await pump();
}
}
@@ -661,10 +661,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> changeFieldTypeOfFieldWithName(
String name,
FieldType type,
) async {
FieldType type, {
ViewLayoutPB layout = ViewLayoutPB.Grid,
}) async {
await tapGridFieldWithName(name);
await tapEditFieldButton();
if (layout == ViewLayoutPB.Grid) {
await tapEditFieldButton();
}
await tapSwitchFieldTypeButton();
await selectFieldType(type);
@@ -881,8 +884,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
}
Future<void> createField(FieldType fieldType, String name) async {
await scrollToRight(find.byType(GridPage));
Future<void> createField(
FieldType fieldType,
String name, {
ViewLayoutPB layout = ViewLayoutPB.Grid,
}) async {
if (layout == ViewLayoutPB.Grid) {
await scrollToRight(find.byType(GridPage));
}
await tapNewPropertyButton();
await renameField(name);
await tapSwitchFieldTypeButton();
@@ -1463,7 +1472,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board =>
expect(find.byType(BoardPage), findsOneWidget),
expect(find.byType(DesktopBoardPage), findsOneWidget),
DatabaseLayoutPB.Calendar =>
expect(find.byType(CalendarPage), findsOneWidget),
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
@@ -1521,7 +1530,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board => find.byType(BoardPage),
DatabaseLayoutPB.Board => find.byType(DesktopBoardPage),
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
DatabaseLayoutPB.Grid => find.byType(GridPage),
_ => throw Exception('Unknown database layout type: $layout'),
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
@@ -12,8 +11,10 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@@ -183,25 +184,35 @@ extension Expectation on WidgetTester {
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
if (PlatformExtension.isDesktop) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
return find.byWidgetPredicate(
(widget) =>
widget is SingleMobileInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
@@ -3,9 +3,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -13,6 +14,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
extension AppFlowySettings on WidgetTester {
/// Open settings page
@@ -37,10 +39,14 @@ extension AppFlowySettings on WidgetTester {
/// Restore the AppFlowy data storage location
Future<void> restoreLocation() async {
final button =
find.byTooltip(LocaleKeys.settings_files_recoverLocationTooltips.tr());
final button = find.text(LocaleKeys.settings_common_reset.tr());
expect(button, findsOneWidget);
await tapButton(button);
await pumpAndSettle();
final confirmButton = find.text(LocaleKeys.button_confirm.tr());
expect(confirmButton, findsOneWidget);
await tapButton(confirmButton);
return;
}
@@ -77,12 +83,21 @@ extension AppFlowySettings on WidgetTester {
// go to settings page and toggle enable RTL toolbar items
Future<void> toggleEnableRTLToolbarItems() async {
await openSettings();
await openSettingsPage(SettingsPage.appearance);
await openSettingsPage(SettingsPage.workspace);
final switchButton =
find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
expect(switchButton, findsOneWidget);
await tapButton(switchButton);
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
find.byType(EnableRTLItemsSwitcher),
0,
scrollable: scrollable,
);
final switcher = find.descendant(
of: find.byType(EnableRTLItemsSwitcher),
matching: find.byType(Toggle),
);
await tap(switcher);
// tap anywhere to close the settings page
await tapAt(Offset.zero);
@@ -70,4 +70,9 @@ class KVKeys {
/// The workspace id is a string.
@Deprecated('deprecated in version 0.5.5')
static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';
/// The key for saving the scale factor
///
/// The value is a double string.
static const String scaleFactor = 'scaleFactor';
}
@@ -1,6 +1,15 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/window_title_bar.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:flutter_bloc/flutter_bloc.dart';
class CocoaWindowChannel {
CocoaWindowChannel._();
@@ -26,9 +35,14 @@ class CocoaWindowChannel {
}
class MoveWindowDetector extends StatefulWidget {
const MoveWindowDetector({super.key, this.child});
const MoveWindowDetector({
super.key,
this.child,
this.showTitleBar = false,
});
final Widget? child;
final bool showTitleBar;
@override
MoveWindowDetectorState createState() => MoveWindowDetectorState();
@@ -40,15 +54,32 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
@override
Widget build(BuildContext context) {
if (!Platform.isMacOS) {
if (!Platform.isMacOS && !Platform.isWindows) {
return widget.child ?? const SizedBox.shrink();
}
if (Platform.isWindows) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showTitleBar) ...[
WindowTitleBar(
leftChildren: [
_buildToggleMenuButton(context),
],
),
] else ...[
const SizedBox(height: 5),
],
widget.child ?? const SizedBox.shrink(),
],
);
}
return GestureDetector(
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
behavior: HitTestBehavior.translucent,
onDoubleTap: () async {
await CocoaWindowChannel.instance.zoom();
},
onDoubleTap: () async => CocoaWindowChannel.instance.zoom(),
onPanStart: (DragStartDetails details) {
winX = details.globalPosition.dx;
winY = details.globalPosition.dy;
@@ -65,4 +96,29 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
child: widget.child,
);
}
Widget _buildToggleMenuButton(BuildContext context) {
if (!context.read<HomeSettingBloc>().state.isMenuCollapsed) {
return const SizedBox.shrink();
}
return FlowyTooltip(
richMessage: TextSpan(
children: [
TextSpan(text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n'),
const TextSpan(text: 'Ctrl+\\'),
],
),
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
? const FlowySvg(FlowySvgs.show_menu_s)
: const FlowySvg(FlowySvgs.hide_menu_m),
),
);
}
}
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:scaled_app/scaled_app.dart';
import 'startup/startup.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
await runAppFlowy();
}
@@ -0,0 +1,106 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'mobile_view_page_bloc.freezed.dart';
class MobileViewPageBloc
extends Bloc<MobileViewPageEvent, MobileViewPageState> {
MobileViewPageBloc({
required this.viewId,
}) : _viewListener = ViewListener(viewId: viewId),
super(MobileViewPageState.initial()) {
on<MobileViewPageEvent>(
(event, emit) async {
await event.when(
initial: () async {
_registerListeners();
final userProfilePB =
await UserBackendService.getCurrentUserProfile()
.fold((s) => s, (f) => null);
final result = await ViewBackendService.getView(viewId);
final isImmersiveMode =
_isImmersiveMode(result.fold((s) => s, (f) => null));
emit(
state.copyWith(
isLoading: false,
result: result,
isImmersiveMode: isImmersiveMode,
userProfilePB: userProfilePB,
),
);
},
updateImmersionMode: (isImmersiveMode) {
emit(
state.copyWith(
isImmersiveMode: isImmersiveMode,
),
);
},
);
},
);
}
final String viewId;
final ViewListener _viewListener;
@override
Future<void> close() {
_viewListener.stop();
return super.close();
}
void _registerListeners() {
_viewListener.start(
onViewUpdated: (view) {
final isImmersiveMode = _isImmersiveMode(view);
add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode));
},
);
}
// only the document page supports immersive mode (version 0.5.6)
bool _isImmersiveMode(ViewPB? view) {
if (view == null) {
return false;
}
final cover = view.cover;
if (cover == null || cover.type == PageStyleCoverImageType.none) {
return false;
} else if (view.layout == ViewLayoutPB.Document && !cover.isPresets) {
// only support immersive mode for document layout
return true;
}
return false;
}
}
@freezed
class MobileViewPageEvent with _$MobileViewPageEvent {
const factory MobileViewPageEvent.initial() = Initial;
const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) =
UpdateImmersionMode;
}
@freezed
class MobileViewPageState with _$MobileViewPageState {
const factory MobileViewPageState({
@Default(true) bool isLoading,
@Default(null) FlowyResult<ViewPB, FlowyError>? result,
@Default(false) bool isImmersiveMode,
@Default(null) UserProfilePB? userProfilePB,
}) = _MobileViewPageState;
factory MobileViewPageState.initial() => const MobileViewPageState();
}
@@ -37,6 +37,7 @@ class FlowyAppBar extends AppBar {
Widget? title,
String? titleText,
FlowyAppBarLeadingType leadingType = FlowyAppBarLeadingType.back,
double? leadingWidth,
Widget? leading,
super.centerTitle,
VoidCallback? onTapLeading,
@@ -52,7 +53,7 @@ class FlowyAppBar extends AppBar {
titleSpacing: 0,
elevation: 0,
leading: leading ?? leadingType.getWidget(onTapLeading),
leadingWidth: leadingType.width,
leadingWidth: leadingWidth ?? leadingType.width,
toolbarHeight: 44.0,
bottom: showDivider
? const PreferredSize(
@@ -1,14 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/plugins/shared/sync_indicator.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
@@ -16,16 +13,12 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPage extends StatefulWidget {
const MobileViewPage({
@@ -47,94 +40,33 @@ class MobileViewPage extends StatefulWidget {
}
class _MobileViewPageState extends State<MobileViewPage> {
late final Future<FlowyResult<ViewPB, FlowyError>> future;
// used to determine if the user has scrolled down and show the app bar in immersive mode
ScrollNotificationObserverState? _scrollNotificationObserver;
// control the app bar opacity when in immersive mode
final ValueNotifier<double> _appBarOpacity = ValueNotifier(0.0);
// only enable immersive mode for document layout
final ValueNotifier<bool> _isImmersiveMode = ValueNotifier(false);
ViewListener? viewListener;
@override
void initState() {
super.initState();
future = ViewBackendService.getView(widget.id);
}
final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);
@override
void dispose() {
_appBarOpacity.dispose();
_isImmersiveMode.dispose();
viewListener?.stop();
_scrollNotificationObserver = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final child = FutureBuilder(
future: future,
builder: (context, state) {
Widget body;
ViewPB? viewPB;
final actions = <Widget>[];
if (state.connectionState != ConnectionState.done) {
body = const Center(
child: CircularProgressIndicator(),
);
} else if (!state.hasData) {
body = FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: state.error.toString(),
);
} else {
body = state.data!.fold((view) {
viewPB = view;
_updateImmersiveMode(view);
viewListener?.stop();
viewListener = ViewListener(viewId: view.id)
..start(
onViewUpdated: _updateImmersiveMode,
);
return BlocProvider(
create: (_) => MobileViewPageBloc(viewId: widget.id)
..add(const MobileViewPageEvent.initial()),
child: BlocBuilder<MobileViewPageBloc, MobileViewPageState>(
builder: (context, state) {
final view = state.result?.fold((s) => s, (f) => null);
final body = _buildBody(context, state);
actions.addAll([
if (FeatureFlag.syncDocument.isOn) ...[
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
view.layout == ViewLayoutPB.Document
? DocumentSyncIndicator(view: view)
: DatabaseSyncIndicator(view: view),
const HSpace(8.0),
],
_buildAppBarLayoutButton(view),
_buildAppBarMoreButton(view),
]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
}, (error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
});
}
if (view == null) {
return _buildApp(context, null, body);
}
if (viewPB != null) {
return MultiBlocProvider(
providers: [
BlocProvider(
@@ -143,47 +75,167 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
BlocProvider(
create: (_) =>
ViewBloc(view: viewPB!)..add(const ViewEvent.initial()),
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
if (viewPB!.layout == ViewLayoutPB.Document)
if (view.layout.isDocumentView)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: viewPB!)
..add(
const DocumentPageStyleEvent.initial(),
),
create: (_) => DocumentPageStyleBloc(view: view)
..add(const DocumentPageStyleEvent.initial()),
),
],
child: Builder(
builder: (context) {
final view = context.watch<ViewBloc>().state.view;
return _buildApp(view, actions, body);
return _buildApp(context, view, body);
},
),
);
} else {
return _buildApp(null, [], body);
}
},
},
),
);
return child;
}
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
// only enable immersive mode for document layout
final isImmersive = view?.layout == ViewLayoutPB.Document;
Widget _buildApp(
BuildContext context,
ViewPB? view,
Widget child,
) {
final isDocument = view?.layout.isDocumentView ?? false;
final title = _buildTitle(context, view);
final actions = _buildAppBarActions(context, view);
final appBar = isDocument
? MobileViewPageImmersiveAppBar(
preferredSize: Size(
double.infinity,
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
),
title: title,
appBarOpacity: _appBarOpacity,
actions: actions,
)
: FlowyAppBar(title: title, actions: actions);
final body = isDocument
? Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
)
: child;
return Scaffold(
extendBodyBehindAppBar: isDocument,
appBar: appBar,
body: body,
);
}
Widget _buildBody(BuildContext context, MobileViewPageState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final result = state.result;
if (result == null) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: '',
);
}
return result.fold(
(view) {
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
},
(error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
},
);
}
// Document:
// - [ collaborators, sync_indicator, layout_button, more_button]
// Database:
// - [ sync_indicator, more_button]
List<Widget> _buildAppBarActions(BuildContext context, ViewPB? view) {
if (view == null) {
return [];
}
final isImmersiveMode =
context.read<MobileViewPageBloc>().state.isImmersiveMode;
final actions = <Widget>[];
if (FeatureFlag.syncDocument.isOn) {
// only document supports displaying collaborators.
if (view.layout.isDocumentView) {
actions.addAll([
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
DocumentSyncIndicator(view: view),
const HSpace(12.0),
]);
} else {
actions.addAll([
DatabaseSyncIndicator(view: view),
const HSpace(12.0),
]);
}
}
if (view.layout.isDocumentView) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
}
actions.addAll([
MobileViewPageMoreButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
return actions;
}
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon.value;
final title = Row(
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 34.0),
child: EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
),
Expanded(
child: FlowyText.medium(
@@ -194,55 +246,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
],
);
if (isImmersive) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: Size(
double.infinity,
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
),
child: ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leading: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0),
child: AppBarButton(
padding: EdgeInsets.zero,
onTap: (context) => context.pop(),
child: _buildImmersiveAppBarIcon(
FlowySvgs.m_app_bar_back_s,
30.0,
iconPadding: 6.0,
),
),
),
actions: actions,
),
),
),
body: Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
),
);
}
return Scaffold(
appBar: FlowyAppBar(
title: title,
actions: actions,
),
body: child,
);
}
void _rebuildScrollNotificationObserver(BuildContext context) {
@@ -251,163 +254,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
_scrollNotificationObserver?.addListener(_onScrollNotification);
}
Widget _buildAppBarLayoutButton(ViewPB view) {
// only display the layout button if the view is a document
if (view.layout != ViewLayoutPB.Document) {
return const SizedBox.shrink();
}
return AppBarButton(
padding: const EdgeInsets.symmetric(vertical: 2.0),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.pageStyle_title.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: PageStyleBottomSheet(
view: context.read<ViewBloc>().state.view,
),
),
);
},
child: _buildImmersiveAppBarIcon(FlowySvgs.m_layout_s, 30.0),
);
}
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => _buildAppBarMoreBottomSheet(context),
);
},
child: _buildImmersiveAppBarIcon(FlowySvgs.m_app_bar_more_s, 30.0),
);
}
Widget _buildImmersiveAppBarIcon(
FlowySvgData icon,
double dimension, {
double iconPadding = 5.0,
}) {
assert(
dimension > 0.0 && dimension <= kToolbarHeight,
'dimension must be greater than 0, and less than or equal to kToolbarHeight',
);
return UnconstrainedBox(
child: SizedBox.square(
dimension: dimension,
child: ValueListenableBuilder(
valueListenable: _isImmersiveMode,
builder: (context, isImmersiveMode, child) {
return ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (context, appBarOpacity, child) {
Color? color;
// if there's no cover or the cover is not immersive,
// make sure the app bar is always visible
if (!isImmersiveMode) {
color = null;
} else if (appBarOpacity < 0.99) {
color = Colors.white;
}
Widget child = Container(
margin: EdgeInsets.all(iconPadding),
child: FlowySvg(
icon,
color: color,
),
);
if (isImmersiveMode && appBarOpacity <= 0.99) {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
}
return child;
},
);
},
),
),
);
}
Widget _buildAppBarMoreBottomSheet(BuildContext context) {
final view = context.read<ViewBloc>().state.view;
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
// immersive mode related
// auto show or hide the app bar based on the scroll position
void _onScrollNotification(ScrollNotification notification) {
@@ -418,7 +264,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
if (notification is ScrollUpdateNotification &&
defaultScrollNotificationPredicate(notification)) {
final ScrollMetrics metrics = notification.metrics;
final height = MediaQuery.of(context).padding.top;
double height = MediaQuery.of(context).padding.top;
if (defaultTargetPlatform == TargetPlatform.android) {
height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight;
}
final progress = (metrics.pixels / height).clamp(0.0, 1.0);
// reduce the sensitivity of the app bar opacity change
if ((progress - _appBarOpacity.value).abs() >= 0.1 ||
@@ -428,16 +277,4 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
}
}
void _updateImmersiveMode(ViewPB view) {
final cover = view.cover;
if (cover == null || cover.type == PageStyleCoverImageType.none) {
_isImmersiveMode.value = false;
} else if (view.layout != ViewLayoutPB.Document) {
// only support immersive mode for document layout
_isImmersiveMode.value = false;
} else {
_isImmersiveMode.value = true;
}
}
}
@@ -0,0 +1,231 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageImmersiveAppBar extends StatelessWidget
implements PreferredSizeWidget {
const MobileViewPageImmersiveAppBar({
super.key,
required this.preferredSize,
required this.appBarOpacity,
required this.title,
required this.actions,
});
final ValueListenable appBarOpacity;
final Widget title;
final List<Widget> actions;
@override
final Size preferredSize;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leadingWidth: 44,
leading: Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0),
child: _buildAppBarBackButton(context),
),
actions: actions,
),
);
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
onTap: (context) => context.pop(),
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_back_s,
dimension: 30.0,
iconPadding: 3.0,
isImmersiveMode:
context.read<MobileViewPageBloc>().state.isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageMoreButton extends StatelessWidget {
const MobileViewPageMoreButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<ViewBloc>()),
BlocProvider.value(value: context.read<FavoriteBloc>()),
],
child: MobileViewPageMoreBottomSheet(view: view),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_more_s,
dimension: 30.0,
iconPadding: 3.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageLayoutButton extends StatelessWidget {
const MobileViewPageLayoutButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
// only display the layout button if the view is a document
if (view.layout != ViewLayoutPB.Document) {
return const SizedBox.shrink();
}
return AppBarButton(
padding: const EdgeInsets.symmetric(vertical: 2.0),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.pageStyle_title.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<DocumentPageStyleBloc>()),
BlocProvider.value(value: context.read<MobileViewPageBloc>()),
],
child: PageStyleBottomSheet(
view: context.read<ViewBloc>().state.view,
),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_layout_s,
dimension: 30.0,
iconPadding: 3.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class _ImmersiveAppBarButton extends StatelessWidget {
const _ImmersiveAppBarButton({
required this.icon,
required this.dimension,
required this.iconPadding,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final FlowySvgData icon;
final double dimension;
final double iconPadding;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
assert(
dimension > 0.0 && dimension <= kToolbarHeight,
'dimension must be greater than 0, and less than or equal to kToolbarHeight',
);
// if the immersive mode is on, the icon should be white and add a black background
// also, the icon opacity will change based on the app bar opacity
return UnconstrainedBox(
child: SizedBox.square(
dimension: dimension,
child: ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (context, appBarOpacity, child) {
Color? color;
// if there's no cover or the cover is not immersive,
// make sure the app bar is always visible
if (!isImmersiveMode) {
color = null;
} else if (appBarOpacity < 0.99) {
color = Colors.white;
}
Widget child = Container(
margin: EdgeInsets.all(iconPadding),
child: FlowySvg(icon, color: color),
);
if (isImmersiveMode && appBarOpacity <= 0.99) {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
}
return child;
},
),
),
);
}
}
@@ -0,0 +1,67 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageMoreBottomSheet extends StatelessWidget {
const MobileViewPageMoreBottomSheet({super.key, required this.view});
final ViewPB view;
@override
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
@@ -1,4 +1,4 @@
export 'mobile_board_screen.dart';
export 'mobile_board_content.dart';
export 'mobile_board_page.dart';
export 'widgets/mobile_hidden_groups_column.dart';
export 'widgets/mobile_board_trailing.dart';
@@ -3,12 +3,16 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -16,25 +20,100 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileBoardContent extends StatefulWidget {
const MobileBoardContent({
class MobileBoardPage extends StatefulWidget {
const MobileBoardPage({
super.key,
required this.view,
required this.databaseController,
this.onEditStateChanged,
});
final ViewPB view;
final DatabaseController databaseController;
/// Called when edit state changed
final VoidCallback? onEditStateChanged;
@override
State<MobileBoardContent> createState() => _MobileBoardContentState();
State<MobileBoardPage> createState() => _MobileBoardPageState();
}
class _MobileBoardContentState extends State<MobileBoardContent> {
class _MobileBoardPageState extends State<MobileBoardPage> {
late final ValueNotifier<DidCreateRowResult?> _didCreateRow;
@override
void initState() {
super.initState();
_didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);
}
@override
void dispose() {
_didCreateRow
..removeListener(_handleDidCreateRow)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<BoardBloc>(
create: (_) => BoardBloc(
databaseController: widget.databaseController,
didCreateRow: _didCreateRow,
)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) => state.maybeMap(
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
error: (err) => FlowyMobileStateContainer.error(
emoji: '🛸',
title: LocaleKeys.board_mobile_failedToLoad.tr(),
errorMsg: err.toString(),
),
ready: (data) => const _BoardContent(),
orElse: () => const SizedBox.shrink(),
),
),
);
}
void _handleDidCreateRow() {
if (_didCreateRow.value != null) {
final result = _didCreateRow.value!;
switch (result.action) {
case DidCreateRowAction.openAsPage:
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: result.rowMeta.id,
MobileRowDetailPage.argDatabaseController:
widget.databaseController,
},
);
break;
default:
break;
}
}
}
}
class _BoardContent extends StatefulWidget {
const _BoardContent();
@override
State<_BoardContent> createState() => _BoardContentState();
}
class _BoardContentState extends State<_BoardContent> {
late final ScrollController scrollController;
late final AppFlowyBoardScrollController scrollManager;
@override
void initState() {
super.initState();
// mobile may not need this
// scroll to bottom when add a new card
scrollManager = AppFlowyBoardScrollController();
scrollController = ScrollController();
}
@@ -57,54 +136,48 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
cardMargin: const EdgeInsets.all(4),
);
return BlocListener<BoardBloc, BoardState>(
listenWhen: (previous, current) =>
previous.recentAddedRowMeta != current.recentAddedRowMeta,
listener: (context, state) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: state.recentAddedRowMeta!.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
final showCreateGroupButton = context
.read<BoardBloc>()
.groupingFieldType
?.canCreateNewGroup ??
false;
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
return AppFlowyBoard(
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints:
BoxConstraints.tightFor(width: screenWidth * 0.7),
config: config,
leading: showHiddenGroups
? MobileHiddenGroupsColumn(
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
afGroupData: column,
afGroupItem: columnItem,
cardMargin: config.cardMargin,
),
);
},
);
},
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
config: config,
leading: showHiddenGroups
? MobileHiddenGroupsColumn(
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
afGroupData: column,
afGroupItem: columnItem,
cardMargin: config.cardMargin,
),
);
},
),
);
}
@@ -129,9 +202,14 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
color: style.colorScheme.onSurface,
),
),
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.createBottomRow(columnData.id)),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
),
),
);
}
@@ -146,16 +224,9 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
final groupItem = afGroupItem as GroupItem;
final groupData = afGroupData.customData as GroupData;
final rowMeta = groupItem.row;
final rowCache = boardBloc.getRowCache();
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
final viewId = boardBloc.viewId;
final cellBuilder =
CardCellBuilder(databaseController: boardBloc.databaseController);
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
final groupItemId = groupItem.row.id + groupData.group.groupId;
@@ -166,12 +237,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: isEditing,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) {
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
@@ -181,10 +252,8 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
},
);
},
onStartEditing: () => boardBloc
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
onEndEditing: () =>
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
@@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -73,8 +71,12 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
);
}
if (state.isEditingHeader &&
state.editingHeaderId == widget.groupData.id) {
final isEditing = state.maybeMap(
ready: (value) => value.editingHeaderId == widget.groupData.id,
orElse: () => false,
);
if (isEditing) {
title = TextField(
controller: _controller,
autofocus: true,
@@ -135,7 +137,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
icon: FlowySvgs.hide_s,
onTap: () {
context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
BoardEvent.setGroupVisibility(
widget.groupData.customData.group
as GroupPB,
false,
@@ -154,9 +156,16 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
color: Theme.of(context).colorScheme.onSurface,
),
splashRadius: 5,
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(widget.groupData.id),
),
onPressed: () {
context.read<BoardBloc>().add(
BoardEvent.createRow(
widget.groupData.id,
OrderObjectPositionTypePB.Start,
null,
null,
),
);
},
),
],
),
@@ -23,7 +23,10 @@ class MobileHiddenGroupsColumn extends StatelessWidget {
Widget build(BuildContext context) {
final databaseController = context.read<BoardBloc>().databaseController;
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
selector: (state) => state.layoutSettings,
selector: (state) => state.maybeMap(
orElse: () => null,
ready: (value) => value.layoutSettings,
),
builder: (context, layoutSettings) {
if (layoutSettings == null) {
return const SizedBox.shrink();
@@ -105,29 +108,36 @@ class MobileHiddenGroupList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => MobileHiddenGroup(
key: ValueKey(state.hiddenGroups[index].groupId),
group: state.hiddenGroups[index],
index: index,
),
proxyDecorator: (child, index, animation) => BlocProvider.value(
value: context.read<BoardBloc>(),
child: Material(color: Colors.transparent, child: child),
),
physics: const ClampingScrollPhysics(),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
builder: (_, state) {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
return ReorderableListView.builder(
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => MobileHiddenGroup(
key: ValueKey(state.hiddenGroups[index].groupId),
group: state.hiddenGroups[index],
index: index,
),
proxyDecorator: (child, index, animation) => BlocProvider.value(
value: context.read<BoardBloc>(),
child: Material(color: Colors.transparent, child: child),
),
physics: const ClampingScrollPhysics(),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
);
},
);
},
);
}
}
@@ -148,92 +158,77 @@ class MobileHiddenGroup extends StatelessWidget {
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == this.group.groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = group.rows.map(
(item) {
final cellContext =
databaseController.rowCache.loadCells(item).firstWhere(
(cellContext) => cellContext.fieldId == primaryField.id,
);
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
child: CardCellBuilder(
databaseController:
context.read<BoardBloc>().databaseController,
).build(
cellContext: cellContext,
styleMap: {FieldType.RichText: _titleCellStyle(context)},
hasNotes: !item.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
final cells = group.rows.map(
(item) {
final cellContext =
databaseController.rowCache.loadCells(item).firstWhere(
(cellContext) => cellContext.fieldId == primaryField.id,
);
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
child: CardCellBuilder(
databaseController: context.read<BoardBloc>().databaseController,
).build(
cellContext: cellContext,
styleMap: {FieldType.RichText: _titleCellStyle(context)},
hasNotes: !item.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
).toList();
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Row(
children: [
Expanded(
child: Text(
context.read<BoardBloc>().generateGroupNameFromGroup(group),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
child: const Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.hide_m,
size: Size.square(20),
),
),
onTap: () => showFlowyMobileConfirmDialog(
context,
title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()),
content: FlowyText(
LocaleKeys.board_mobile_showGroupContent.tr(),
),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.primary,
onActionButtonPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
),
],
),
children: cells,
);
},
).toList();
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Row(
children: [
Expanded(
child: Text(
context.read<BoardBloc>().generateGroupNameFromGroup(group),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
child: const Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.hide_m,
size: Size.square(20),
),
),
onTap: () => showFlowyMobileConfirmDialog(
context,
title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()),
content: FlowyText(
LocaleKeys.board_mobile_showGroupContent.tr(),
),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.primary,
onActionButtonPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.setGroupVisibility(group, true)),
),
),
],
),
children: cells,
);
}
@@ -162,7 +162,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
}
deleteRow
? RowBackendService.deleteRow(viewId, rowId)
? RowBackendService.deleteRows(viewId, [rowId])
: RowBackendService.duplicateRow(viewId, rowId);
context
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -16,7 +16,7 @@ class MobileCardContent extends StatelessWidget {
final RowMetaPB rowMeta;
final CardCellBuilder cellBuilder;
final List<CellContext> cells;
final List<CellMeta> cells;
final RowCardStyleConfiguration styleConfiguration;
@override
@@ -26,9 +26,9 @@ class MobileCardContent extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: cells.map(
(cellContext) {
(cellMeta) {
return cellBuilder.build(
cellContext: cellContext,
cellContext: cellMeta.cellContext(),
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
);
@@ -127,6 +127,12 @@ class _MobileWorkspace extends StatelessWidget {
workspace: currentWorkspace,
iconSize: 26,
enableEdit: false,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
result.emoji,
),
),
),
),
const HSpace(8),
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Only works on mobile.
@@ -105,6 +107,12 @@ class _WorkspaceMenuItem extends StatelessWidget {
enableEdit: false,
iconSize: 26,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
@@ -29,9 +29,7 @@ class FontPickerScreen extends StatelessWidget {
}
class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({
super.key,
});
const LanguagePickerPage({super.key});
@override
State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@@ -43,7 +41,6 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
@override
void initState() {
super.initState();
availableFonts = _availableFonts;
}
@@ -90,7 +87,6 @@ class _FontSelectorState extends State<FontSelector> {
@override
void initState() {
super.initState();
availableFonts = _availableFonts;
}
@@ -3,6 +3,7 @@ import 'dart:io';
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:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
@@ -83,7 +84,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
},
itemBuilder: (context, emojiId, emoji, callback) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(2.0),
iconPadding: PlatformExtension.isWindows
? const EdgeInsets.only(bottom: 2.0)
: const EdgeInsets.all(2),
icon: FlowyText(
emoji,
fontSize: 28.0,
@@ -48,7 +48,7 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
emit(state.copyWith(rows: const []));
return;
}
final payload = RepeatedRowIdPB(
final payload = GetRelatedRowDataPB(
databaseId: state.relatedDatabaseMeta!.databaseId,
rowIds: cellData.rowIds,
);
@@ -1,6 +1,7 @@
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -12,11 +13,9 @@ class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
}
void _loadDatabaseMetas() async {
final getDatabaseResult = await DatabaseEventGetDatabases().send();
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
(s) => s.items,
(f) => [],
);
final metaPBs = await DatabaseEventGetDatabases()
.send()
.fold<List<DatabaseMetaPB>>((s) => s.items, (f) => []);
final futures = metaPBs.map((meta) {
return ViewBackendService.getView(meta.inlineViewId).then(
(result) => result.fold(
@@ -255,9 +255,7 @@ class RowCache {
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
return RowInfo(
viewId: viewId,
fields: _fieldDelegate.fieldInfos,
rowId: rowMetaPB.id,
rowMeta: rowMetaPB,
);
}
@@ -285,12 +283,13 @@ class RowChangesetNotifier extends ChangeNotifier {
@unfreezed
class RowInfo with _$RowInfo {
const RowInfo._();
factory RowInfo({
required String rowId,
required String viewId,
required UnmodifiableListView<FieldInfo> fields,
required RowMetaPB rowMeta,
}) = _RowInfo;
String get rowId => rowMeta.id;
}
typedef InsertedIndexs = List<InsertedIndex>;
@@ -96,15 +96,15 @@ class RowBackendService {
return DatabaseEventUpdateRowMeta(payload).send();
}
static Future<FlowyResult<void, FlowyError>> deleteRow(
static Future<FlowyResult<void, FlowyError>> deleteRows(
String viewId,
RowId rowId,
List<RowId> rowIds,
) {
final payload = RowIdPB.create()
final payload = RepeatedRowIdPB.create()
..viewId = viewId
..rowId = rowId;
..rowIds.addAll(rowIds);
return DatabaseEventDeleteRow(payload).send();
return DatabaseEventDeleteRows(payload).send();
}
static Future<FlowyResult<void, FlowyError>> duplicateRow(
@@ -0,0 +1,93 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'board_actions_bloc.freezed.dart';
class BoardActionsCubit extends Cubit<BoardActionsState> {
BoardActionsCubit({
required this.databaseController,
}) : super(const BoardActionsState.initial());
final DatabaseController databaseController;
void startEditingRow(GroupedRowId groupedRowId) {
emit(BoardActionsState.startEditingRow(groupedRowId: groupedRowId));
emit(const BoardActionsState.initial());
}
void endEditing(GroupedRowId groupedRowId) {
emit(const BoardActionsState.endEditingRow());
emit(BoardActionsState.setFocus(groupedRowIds: [groupedRowId]));
emit(const BoardActionsState.initial());
}
void openCard(RowMetaPB rowMeta) {
emit(BoardActionsState.openCard(rowMeta: rowMeta));
emit(const BoardActionsState.initial());
}
void openCardWithRowId(rowId) {
final rowMeta = databaseController.rowCache.getRow(rowId)!.rowMeta;
openCard(rowMeta);
}
void setFocus(List<GroupedRowId> groupedRowIds) {
emit(BoardActionsState.setFocus(groupedRowIds: groupedRowIds));
emit(const BoardActionsState.initial());
}
void startCreateBottomRow(String groupId) {
emit(BoardActionsState.startCreateBottomRow(groupId: groupId));
emit(const BoardActionsState.initial());
}
void createRow(
GroupedRowId? groupedRowId,
CreateBoardCardRelativePosition relativePosition,
) {
emit(
BoardActionsState.createRow(
groupedRowId: groupedRowId,
position: relativePosition,
),
);
emit(const BoardActionsState.initial());
}
}
@freezed
class BoardActionsState with _$BoardActionsState {
const factory BoardActionsState.initial() = _BoardActionsInitialState;
const factory BoardActionsState.openCard({
required RowMetaPB rowMeta,
}) = _BoardActionsOpenCardState;
const factory BoardActionsState.startEditingRow({
required GroupedRowId groupedRowId,
}) = _BoardActionsStartEditingRowState;
const factory BoardActionsState.endEditingRow() =
_BoardActionsEndEditingRowState;
const factory BoardActionsState.setFocus({
required List<GroupedRowId> groupedRowIds,
}) = _BoardActionsSetFocusState;
const factory BoardActionsState.startCreateBottomRow({
required String groupId,
}) = _BoardActionsStartCreateBottomRowState;
const factory BoardActionsState.createRow({
required GroupedRowId? groupedRowId,
required CreateBoardCardRelativePosition position,
}) = _BoardActionCreateRowState;
}
enum CreateBoardCardRelativePosition {
before,
after,
}
@@ -8,10 +8,11 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -27,95 +28,108 @@ part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> {
BoardBloc({
required ViewPB view,
required this.databaseController,
}) : super(BoardState.initial(view.id)) {
this.didCreateRow,
AppFlowyBoardController? boardController,
}) : super(const BoardState.loading()) {
groupBackendSvc = GroupBackendService(viewId);
boardController = AppFlowyBoardController(
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
),
onMoveGroupItem: (groupId, fromIndex, toIndex) {
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: groupId,
toGroupId: groupId,
);
}
},
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
}
},
);
_initBoardController(boardController);
_dispatch();
}
final DatabaseController databaseController;
late final AppFlowyBoardController boardController;
final LinkedHashMap<String, GroupController> groupControllers =
LinkedHashMap();
final List<GroupPB> groupList = [];
late final AppFlowyBoardController boardController;
final ValueNotifier<DidCreateRowResult?>? didCreateRow;
late final GroupBackendService groupBackendSvc;
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
void _initBoardController(AppFlowyBoardController? controller) {
boardController = controller ??
AppFlowyBoardController(
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
),
onMoveGroupItem: (groupId, fromIndex, toIndex) {
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: groupId,
toGroupId: groupId,
);
}
},
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
final fromRow =
groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
}
},
);
}
void _dispatch() {
on<BoardEvent>(
(event, emit) async {
await event.when(
initial: () async {
emit(BoardState.initial(viewId));
_startListening();
await _openGrid(emit);
await _openDatabase(emit);
},
createHeaderRow: (groupId) async {
final rowId = groupControllers[groupId]?.firstRow()?.id;
final position = rowId == null
? OrderObjectPositionTypePB.Start
: OrderObjectPositionTypePB.Before;
createRow: (groupId, position, title, targetRowId) async {
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
final void Function(RowDataBuilder)? cellBuilder = title == null
? null
: (builder) => builder.insertText(primaryField, title);
final result = await RowBackendService.createRow(
viewId: databaseController.viewId,
groupId: groupId,
position: position,
targetRowId: rowId,
targetRowId: targetRowId,
withCells: cellBuilder,
);
result.fold(
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
(err) => Log.error(err),
);
},
createBottomRow: (groupId) async {
final rowId = groupControllers[groupId]?.lastRow()?.id;
final position = rowId == null
? OrderObjectPositionTypePB.End
: OrderObjectPositionTypePB.After;
final result = await RowBackendService.createRow(
viewId: databaseController.viewId,
groupId: groupId,
position: position,
targetRowId: rowId,
);
final startEditing = position != OrderObjectPositionTypePB.End;
final action = PlatformExtension.isMobile
? DidCreateRowAction.openAsPage
: startEditing
? DidCreateRowAction.startEditing
: DidCreateRowAction.none;
result.fold(
(rowMeta) => emit(state.copyWith(recentAddedRowMeta: rowMeta)),
(rowMeta) {
state.maybeMap(
ready: (value) {
didCreateRow?.value = DidCreateRowResult(
action: action,
rowMeta: rowMeta,
groupId: groupId,
);
},
orElse: () {},
);
},
(err) => Log.error(err),
);
},
@@ -127,89 +141,62 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
result.fold((_) {}, (err) => Log.error(err));
},
didCreateRow: (group, row, int? index) {
emit(
state.copyWith(
isEditingRow: true,
editingRow: BoardEditingRow(
group: group,
row: row,
index: index,
),
),
);
_groupItemStartEditing(group, row, true);
},
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: grid));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: error));
didReceiveError: (error) {
emit(BoardState.error(error: error));
},
didReceiveGroups: (List<GroupPB> groups) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
emit(
state.copyWith(
hiddenGroups: hiddenGroups,
groupIds: groups.map((group) => group.groupId).toList(),
),
state.maybeMap(
ready: (state) {
emit(
state.copyWith(
hiddenGroups: _filterHiddenGroups(hideUngrouped, groups),
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
orElse: () {},
);
},
didUpdateLayoutSettings: (layoutSettings) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
emit(
state.copyWith(
layoutSettings: layoutSettings,
hiddenGroups: hiddenGroups,
),
state.maybeMap(
ready: (state) {
emit(
state.copyWith(
layoutSettings: layoutSettings,
hiddenGroups: _filterHiddenGroups(hideUngrouped, groupList),
),
);
},
orElse: () {},
);
},
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
await _toggleGroupVisibility(group, isVisible);
setGroupVisibility: (GroupPB group, bool isVisible) async {
await _setGroupVisibility(group, isVisible);
},
toggleHiddenSectionVisibility: (isVisible) async {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
await state.maybeMap(
ready: (state) async {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.collapseHiddenGroups = isVisible,
);
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.collapseHiddenGroups = isVisible,
);
await databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
await databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
},
orElse: () {},
);
},
reorderGroup: (fromGroupId, toGroupId) async {
_reorderGroup(fromGroupId, toGroupId, emit);
},
startEditingRow: (group, row) {
emit(
state.copyWith(
isEditingRow: true,
editingRow: BoardEditingRow(
group: group,
row: row,
index: null,
),
),
);
_groupItemStartEditing(group, row, true);
},
endEditingRow: (rowId) {
if (state.editingRow != null && state.isEditingRow) {
assert(state.editingRow!.row.id == rowId);
_groupItemStartEditing(
state.editingRow!.group,
state.editingRow!.row,
false,
);
emit(state.copyWith(isEditingRow: false, editingRow: null));
}
},
startEditingHeader: (String groupId) {
emit(
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
state.maybeMap(
ready: (state) => emit(state.copyWith(editingHeaderId: groupId)),
orElse: () {},
);
},
endEditingHeader: (String groupId, String? groupName) async {
@@ -218,41 +205,79 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupId: groupId,
name: groupName,
);
emit(state.copyWith(isEditingHeader: false));
state.maybeMap(
ready: (state) => emit(state.copyWith(editingHeaderId: null)),
orElse: () {},
);
},
deleteCards: (groupedRowIds) async {
final rowIds = groupedRowIds.map((e) => e.rowId).toList();
await RowBackendService.deleteRows(viewId, rowIds);
},
moveGroupToAdjacentGroup: (groupedRowId, toPrevious) async {
final fromRow =
databaseController.rowCache.getRow(groupedRowId.rowId)?.rowMeta;
final currentGroupIndex =
boardController.groupIds.indexOf(groupedRowId.groupId);
final toGroupIndex =
toPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1;
if (fromRow != null &&
toGroupIndex > -1 &&
toGroupIndex < boardController.groupIds.length) {
final toGroupId = boardController.groupDatas[toGroupIndex].id;
final result = await databaseController.moveGroupRow(
fromRow: fromRow,
fromGroupId: groupedRowId.groupId,
toGroupId: toGroupId,
);
result.fold(
(s) {
final previousState = state;
emit(
BoardState.setFocus(
groupedRowIds: [
GroupedRowId(
groupId: toGroupId,
rowId: groupedRowId.rowId,
),
],
),
);
emit(previousState);
},
(f) {},
);
}
},
);
},
);
}
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) {
return Log.warn("fieldInfo should not be null");
}
boardController.enableGroupDragging(!isEdit);
}
Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async {
Future<void> _setGroupVisibility(GroupPB group, bool isVisible) async {
if (group.isDefault) {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
await state.maybeMap(
ready: (state) async {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.hideUngroupedColumn = !isVisible,
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.hideUngroupedColumn = !isVisible,
);
await databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
},
orElse: () {},
);
return databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
} else {
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: group.groupId,
visible: isVisible,
);
}
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: group.groupId,
visible: isVisible,
);
}
void _reorderGroup(
@@ -277,6 +302,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
for (final controller in groupControllers.values) {
await controller.dispose();
}
boardController.dispose();
return super.close();
}
@@ -284,11 +310,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
false;
FieldType get groupingFieldType {
final fieldInfo =
databaseController.fieldController.getField(groupList.first.fieldId)!;
return fieldInfo.fieldType;
FieldType? get groupingFieldType {
if (groupList.isEmpty) {
return null;
}
return databaseController.fieldController
.getField(groupList.first.fieldId)
?.fieldType;
}
void initializeGroups(List<GroupPB> groups) {
@@ -321,16 +349,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}
}
RowCache? getRowCache() => databaseController.rowCache;
RowCache get rowCache => databaseController.rowCache;
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onDatabaseChanged: (database) {
if (!isClosed) {
add(BoardEvent.didReceiveGridUpdate(database));
}
},
);
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
onLayoutSettingsChanged: (layoutSettings) {
if (isClosed) {
@@ -433,7 +454,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutSettingsChanged: onLayoutSettingsChanged,
onGroupChanged: onGroupChanged,
);
@@ -451,31 +471,18 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
return <AppFlowyGroupItem>[...items];
}
Future<void> _openGrid(Emitter<BoardState> emit) async {
final result = await databaseController.open();
result.fold(
(grid) {
databaseController.setIsLoading(false);
emit(
state.copyWith(
loadingState: LoadingState.finish(FlowyResult.success(null)),
),
Future<void> _openDatabase(Emitter<BoardState> emit) {
return databaseController.open().fold(
(datbasePB) => databaseController.setIsLoading(false),
(err) => emit(BoardState.error(error: err)),
);
},
(err) => emit(
state.copyWith(
loadingState: LoadingState.finish(FlowyResult.failure(err)),
),
),
);
}
GroupController _initializeGroupController(GroupPB group) {
final delegate = GroupControllerDelegateImpl(
controller: boardController,
fieldController: fieldController,
onNewColumnItem: (groupId, row, index) =>
add(BoardEvent.didCreateRow(group, row, index)),
onNewColumnItem: (groupId, row, index) {},
);
final controller = GroupController(
@@ -579,71 +586,77 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@freezed
class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = _InitialBoard;
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.createRow(
String groupId,
OrderObjectPositionTypePB position,
String? title,
String? targetRowId,
) = _CreateRow;
const factory BoardEvent.createGroup(String name) = _CreateGroup;
const factory BoardEvent.startEditingHeader(String groupId) =
_StartEditingHeader;
const factory BoardEvent.endEditingHeader(String groupId, String? groupName) =
_EndEditingHeader;
const factory BoardEvent.didCreateRow(
GroupPB group,
RowMetaPB row,
int? index,
) = _DidCreateRow;
const factory BoardEvent.startEditingRow(
GroupPB group,
RowMetaPB row,
) = _StartEditRow;
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
const factory BoardEvent.toggleGroupVisibility(
const factory BoardEvent.setGroupVisibility(
GroupPB group,
bool isVisible,
) = _ToggleGroupVisibility;
) = _SetGroupVisibility;
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
_ToggleHiddenSectionVisibility;
const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
_ReorderGroup;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveGridUpdate(
DatabasePB grid,
) = _DidReceiveGridUpdate;
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
_DidReceiveGroups;
const factory BoardEvent.didUpdateLayoutSettings(
BoardLayoutSettingPB layoutSettings,
) = _DidUpdateLayoutSettings;
const factory BoardEvent.deleteCards(List<GroupedRowId> groupedRowIds) =
_DeleteCards;
const factory BoardEvent.moveGroupToAdjacentGroup(
GroupedRowId groupedRowId,
bool toPrevious,
) = _MoveGroupToAdjacentGroup;
}
@freezed
class BoardState with _$BoardState {
const factory BoardState({
const BoardState._();
const factory BoardState.loading() = _BoardLoadingState;
const factory BoardState.error({
required FlowyError error,
}) = _BoardErrorState;
const factory BoardState.ready({
required String viewId,
required DatabasePB? grid,
required List<String> groupIds,
required bool isEditingHeader,
required bool isEditingRow,
required LoadingState loadingState,
required FlowyError? noneOrError,
required BoardLayoutSettingPB? layoutSettings,
String? editingHeaderId,
BoardEditingRow? editingRow,
RowMetaPB? recentAddedRowMeta,
required List<GroupPB> hiddenGroups,
}) = _BoardState;
String? editingHeaderId,
}) = _BoardReadyState;
factory BoardState.initial(String viewId) => BoardState(
grid: null,
const factory BoardState.setFocus({
required List<GroupedRowId> groupedRowIds,
}) = _BoardSetFocusState;
factory BoardState.initial(String viewId) => BoardState.ready(
viewId: viewId,
groupIds: [],
isEditingHeader: false,
isEditingRow: false,
noneOrError: null,
loadingState: const LoadingState.loading(),
layoutSettings: null,
hiddenGroups: [],
);
bool get isLoading => maybeMap(loading: (_) => true, orElse: () => false);
bool get isError => maybeMap(error: (_) => true, orElse: () => false);
bool get isReady => maybeMap(ready: (_) => true, orElse: () => false);
bool get isSetFocus => maybeMap(setFocus: (_) => true, orElse: () => false);
}
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
@@ -658,7 +671,7 @@ class GroupItem extends AppFlowyGroupItem {
required this.fieldInfo,
bool draggable = true,
}) {
super.draggable = draggable;
super.draggable.value = draggable;
}
final RowMetaPB row;
@@ -668,6 +681,23 @@ class GroupItem extends AppFlowyGroupItem {
String get id => row.id.toString();
}
/// Identifies a card in a database view that has grouping. To support cases
/// in which a card can belong to more than one group at the same time (e.g.
/// FieldType.Multiselect), we include the card's group id as well.
///
class GroupedRowId extends Equatable {
const GroupedRowId({
required this.rowId,
required this.groupId,
});
final String rowId;
final String groupId;
@override
List<Object?> get props => [rowId, groupId];
}
class GroupControllerDelegateImpl extends GroupControllerDelegate {
GroupControllerDelegateImpl({
required this.controller,
@@ -743,18 +773,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
}
class BoardEditingRow {
BoardEditingRow({
required this.group,
required this.row,
required this.index,
});
GroupPB group;
RowMetaPB row;
int? index;
}
class GroupData {
GroupData({
required this.group,
@@ -779,3 +797,21 @@ class CheckboxGroup {
// pub const CHECK: &str = "Yes";
bool get isCheck => group.groupId == "Yes";
}
enum DidCreateRowAction {
none,
openAsPage,
startEditing,
}
class DidCreateRowResult {
DidCreateRowResult({
required this.action,
required this.rowMeta,
required this.groupId,
});
final DidCreateRowAction action;
final RowMetaPB rowMeta;
final String groupId;
}
@@ -1,12 +1,11 @@
import 'dart:collection';
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
@@ -15,7 +14,8 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy/shared/conditional_listenable_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -28,12 +28,13 @@ import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../workspace/application/view/view_bloc.dart';
import '../../widgets/card/card.dart';
import '../../widgets/cell/card_cell_builder.dart';
import '../application/board_bloc.dart';
import 'toolbar/board_setting_bar.dart';
import 'widgets/board_focus_scope.dart';
import 'widgets/board_hidden_groups.dart';
import 'widgets/board_shortcut_container.dart';
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@@ -46,7 +47,17 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
bool shrinkWrap,
String? initialRowId,
) =>
BoardPage(view: view, databaseController: controller);
PlatformExtension.isDesktop
? DesktopBoardPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
)
: MobileBoardPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
@override
Widget settingBar(BuildContext context, DatabaseController controller) =>
@@ -79,12 +90,13 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
ValueKey(controller.viewId);
}
class BoardPage extends StatelessWidget {
BoardPage({
class DesktopBoardPage extends StatefulWidget {
const DesktopBoardPage({
super.key,
required this.view,
required this.databaseController,
this.onEditStateChanged,
}) : super(key: ValueKey(view.id));
});
final ViewPB view;
@@ -93,54 +105,154 @@ class BoardPage extends StatelessWidget {
/// Called when edit state changed
final VoidCallback? onEditStateChanged;
@override
State<DesktopBoardPage> createState() => _DesktopBoardPageState();
}
class _DesktopBoardPageState extends State<DesktopBoardPage> {
late final AppFlowyBoardController _boardController = AppFlowyBoardController(
onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>
widget.databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
),
onMoveGroupItem: (groupId, fromIndex, toIndex) {
final groupControllers = _boardBloc.groupControllers;
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
widget.databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: groupId,
toGroupId: groupId,
);
}
},
onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {
final groupControllers = _boardBloc.groupControllers;
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
if (fromRow != null) {
widget.databaseController.moveGroupRow(
fromRow: fromRow,
toRow: toRow,
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
}
},
onStartDraggingCard: (groupId, index) {
final groupControllers = _boardBloc.groupControllers;
final toRow = groupControllers[groupId]?.rowAtIndex(index);
if (toRow != null) {
_focusScope.clear();
}
},
);
late final _focusScope = BoardFocusScope(
boardController: _boardController,
);
late final BoardBloc _boardBloc;
late final BoardActionsCubit _boardActionsCubit;
late final ValueNotifier<DidCreateRowResult?> _didCreateRow;
@override
void initState() {
super.initState();
_didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);
_boardBloc = BoardBloc(
databaseController: widget.databaseController,
didCreateRow: _didCreateRow,
boardController: _boardController,
)..add(const BoardEvent.initial());
_boardActionsCubit = BoardActionsCubit(
databaseController: widget.databaseController,
);
}
@override
void dispose() {
_focusScope.dispose();
_boardBloc.close();
_boardActionsCubit.close();
_didCreateRow
..removeListener(_handleDidCreateRow)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<BoardBloc>(
create: (context) => BoardBloc(
view: view,
databaseController: databaseController,
)..add(const BoardEvent.initial()),
return MultiBlocProvider(
providers: [
BlocProvider<BoardBloc>.value(
value: _boardBloc,
),
BlocProvider.value(
value: _boardActionsCubit,
),
],
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) => state.loadingState.when(
loading: () => const Center(
builder: (context, state) => state.maybeMap(
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
idle: () => const SizedBox.shrink(),
finish: (result) => result.fold(
(_) => PlatformExtension.isMobile
? const MobileBoardContent()
: DesktopBoardContent(onEditStateChanged: onEditStateChanged),
(err) => PlatformExtension.isMobile
? FlowyMobileStateContainer.error(
emoji: '🛸',
title: LocaleKeys.board_mobile_failedToLoad.tr(),
errorMsg: err.toString(),
)
: FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
error: (err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
orElse: () => _BoardContent(
onEditStateChanged: widget.onEditStateChanged,
focusScope: _focusScope,
boardController: _boardController,
),
),
),
);
}
void _handleDidCreateRow() async {
// work around: wait for the new card to be inserted into the board before enabling edit
await Future.delayed(const Duration(milliseconds: 50));
if (_didCreateRow.value != null) {
final result = _didCreateRow.value!;
switch (result.action) {
case DidCreateRowAction.openAsPage:
_boardActionsCubit.openCard(result.rowMeta);
break;
case DidCreateRowAction.startEditing:
_boardActionsCubit.startEditingRow(
GroupedRowId(
groupId: result.groupId,
rowId: result.rowMeta.id,
),
);
break;
default:
break;
}
}
}
}
class DesktopBoardContent extends StatefulWidget {
const DesktopBoardContent({
super.key,
class _BoardContent extends StatefulWidget {
const _BoardContent({
required this.boardController,
required this.focusScope,
this.onEditStateChanged,
});
final AppFlowyBoardController boardController;
final BoardFocusScope focusScope;
final VoidCallback? onEditStateChanged;
@override
State<DesktopBoardContent> createState() => _DesktopBoardContentState();
State<_BoardContent> createState() => _BoardContentState();
}
class _DesktopBoardContentState extends State<DesktopBoardContent> {
class _BoardContentState extends State<_BoardContent> {
final ScrollController scrollController = ScrollController();
final AppFlowyBoardScrollController scrollManager =
AppFlowyBoardScrollController();
@@ -148,16 +260,19 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
final config = const AppFlowyBoardConfig(
groupMargin: EdgeInsets.symmetric(horizontal: 4),
groupBodyPadding: EdgeInsets.symmetric(horizontal: 4),
groupFooterPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
groupFooterPadding: EdgeInsets.fromLTRB(8, 14, 8, 4),
groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
stretchGroupHeight: false,
);
late final cellBuilder = CardCellBuilder(
databaseController: context.read<BoardBloc>().databaseController,
databaseController: databaseController,
);
DatabaseController get databaseController =>
context.read<BoardBloc>().databaseController;
@override
void dispose() {
scrollController.dispose();
@@ -166,15 +281,49 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
@override
Widget build(BuildContext context) {
return BlocListener<BoardBloc, BoardState>(
listener: (context, state) {
widget.onEditStateChanged?.call();
},
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
return Padding(
return MultiBlocListener(
listeners: [
BlocListener<BoardBloc, BoardState>(
listener: (context, state) {
state.maybeMap(
ready: (value) {
widget.onEditStateChanged?.call();
},
orElse: () {},
);
},
),
BlocListener<BoardActionsCubit, BoardActionsState>(
listener: (context, state) {
state.maybeMap(
openCard: (value) {
_openCard(
context: context,
databaseController:
context.read<BoardBloc>().databaseController,
rowMeta: value.rowMeta,
);
},
setFocus: (value) {
widget.focusScope.focusedGroupedRows = value.groupedRowIds;
},
startEditingRow: (value) {
widget.boardController.enableGroupDragging(false);
widget.focusScope.clear();
},
endEditingRow: (value) {
widget.boardController.enableGroupDragging(true);
},
orElse: () {},
);
},
),
],
child: FocusScope(
autofocus: true,
child: BoardShortcutContainer(
focusScope: widget.focusScope,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: AppFlowyBoard(
boardScrollController: scrollManager,
@@ -183,7 +332,11 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
groupConstraints: const BoxConstraints.tightFor(width: 256),
config: config,
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
trailing: showCreateGroupButton
trailing: context
.read<BoardBloc>()
.groupingFieldType
?.canCreateNewGroup ??
false
? BoardTrailing(scrollController: scrollController)
: const HSpace(40),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
@@ -193,112 +346,369 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
margin: config.groupHeaderPadding,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
footerBuilder: (_, groupData) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<BoardBloc>(),
),
BlocProvider.value(
value: context.read<BoardActionsCubit>(),
),
],
child: BoardColumnFooter(
columnData: groupData,
boardConfig: config,
scrollManager: scrollManager,
),
),
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
providers: [
BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
),
BlocProvider.value(
value: context.read<BoardActionsCubit>(),
),
],
child: _BoardCard(
afGroupData: column,
groupItem: columnItem as GroupItem,
boardConfig: config,
notifier: widget.focusScope,
cellBuilder: cellBuilder,
),
),
),
);
),
),
),
);
}
}
@visibleForTesting
class BoardColumnFooter extends StatefulWidget {
const BoardColumnFooter({
super.key,
required this.columnData,
required this.boardConfig,
required this.scrollManager,
});
final AppFlowyGroupData columnData;
final AppFlowyBoardConfig boardConfig;
final AppFlowyBoardScrollController scrollManager;
@override
State<BoardColumnFooter> createState() => _BoardColumnFooterState();
}
class _BoardColumnFooterState extends State<BoardColumnFooter> {
final TextEditingController _textController = TextEditingController();
late final FocusNode _focusNode;
bool _isCreating = false;
@override
void initState() {
super.initState();
_focusNode = FocusNode(
onKeyEvent: (node, event) {
if (_focusNode.hasFocus &&
event.logicalKey == LogicalKeyboardKey.escape) {
_focusNode.unfocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
)..addListener(() {
if (!_focusNode.hasFocus) {
setState(() => _isCreating = false);
}
});
}
@override
void dispose() {
_textController.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isCreating) {
_focusNode.requestFocus();
}
});
return Padding(
padding: widget.boardConfig.groupFooterPadding,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
child:
_isCreating ? _createCardsTextField() : _startCreatingCardsButton(),
),
);
}
Widget _createCardsTextField() {
const nada = DoNothingAndStopPropagationIntent();
return Shortcuts(
shortcuts: {
const SingleActivator(LogicalKeyboardKey.arrowUp): nada,
const SingleActivator(LogicalKeyboardKey.arrowDown): nada,
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): nada,
const SingleActivator(LogicalKeyboardKey.keyE): nada,
const SingleActivator(LogicalKeyboardKey.keyN): nada,
const SingleActivator(LogicalKeyboardKey.delete): nada,
const SingleActivator(LogicalKeyboardKey.backspace): nada,
const SingleActivator(LogicalKeyboardKey.enter): nada,
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
const SingleActivator(LogicalKeyboardKey.comma): nada,
const SingleActivator(LogicalKeyboardKey.period): nada,
SingleActivator(
LogicalKeyboardKey.arrowUp,
shift: true,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): nada,
},
child: FlowyTextField(
hintTextConstraints: const BoxConstraints(maxHeight: 36),
controller: _textController,
focusNode: _focusNode,
onSubmitted: (name) {
context.read<BoardBloc>().add(
BoardEvent.createRow(
widget.columnData.id,
OrderObjectPositionTypePB.End,
name,
null,
),
);
widget.scrollManager.scrollToBottom(widget.columnData.id);
_textController.clear();
_focusNode.requestFocus();
},
),
);
}
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
return Padding(
padding: config.groupFooterPadding,
Widget _startCreatingCardsButton() {
return BlocListener<BoardActionsCubit, BoardActionsState>(
listener: (context, state) {
state.maybeWhen(
startCreateBottomRow: (groupId) {
if (groupId == widget.columnData.id) {
setState(() => _isCreating = true);
}
},
orElse: () {},
);
},
child: FlowyTooltip(
message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(),
child: FlowyHover(
child: AppFlowyGroupFooter(
height: 36,
icon: FlowySvg(
child: SizedBox(
height: 36,
child: FlowyButton(
leftIcon: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).hintColor,
),
title: FlowyText.medium(
text: FlowyText.medium(
LocaleKeys.board_column_createNewCard.tr(),
color: Theme.of(context).hintColor,
),
onAddButtonClick: () => context
.read<BoardBloc>()
.add(BoardEvent.createBottomRow(columnData.id)),
onTap: () {
setState(() => _isCreating = true);
},
),
),
),
);
}
}
Widget _buildCard(
BuildContext context,
AppFlowyGroupData afGroupData,
AppFlowyGroupItem afGroupItem,
) {
class _BoardCard extends StatefulWidget {
const _BoardCard({
required this.afGroupData,
required this.groupItem,
required this.boardConfig,
required this.cellBuilder,
required this.notifier,
});
final AppFlowyGroupData afGroupData;
final GroupItem groupItem;
final AppFlowyBoardConfig boardConfig;
final CardCellBuilder cellBuilder;
final BoardFocusScope notifier;
@override
State<_BoardCard> createState() => _BoardCardState();
}
class _BoardCardState extends State<_BoardCard> {
bool _isEditing = false;
@override
Widget build(BuildContext context) {
final boardBloc = context.read<BoardBloc>();
final groupItem = afGroupItem as GroupItem;
final groupData = afGroupData.customData as GroupData;
final rowCache = boardBloc.getRowCache();
final rowInfo = rowCache?.getRow(groupItem.row.id);
/// Return placeholder widget if the rowCache or rowInfo is null.
if (rowCache == null) {
return SizedBox.shrink(key: ObjectKey(groupItem));
}
final groupData = widget.afGroupData.customData as GroupData;
final rowCache = boardBloc.rowCache;
final databaseController = boardBloc.databaseController;
final viewId = boardBloc.viewId;
final rowMeta =
rowCache.getRow(widget.groupItem.id)?.rowMeta ?? widget.groupItem.row;
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
const nada = DoNothingAndStopPropagationIntent();
final groupItemId = "${groupData.group.groupId}${groupItem.row.id}";
final rowMeta = rowInfo?.rowMeta ?? groupItem.row;
return BlocListener<BoardActionsCubit, BoardActionsState>(
listener: (context, state) {
state.maybeMap(
startEditingRow: (value) {
if (value.groupedRowId.rowId == widget.groupItem.id &&
value.groupedRowId.groupId == groupData.group.groupId) {
setState(() => _isEditing = true);
}
},
endEditingRow: (_) {
if (_isEditing) {
setState(() => _isEditing = false);
}
},
createRow: (value) {
if ((_isEditing && value.groupedRowId == null) ||
(value.groupedRowId?.rowId == widget.groupItem.id &&
value.groupedRowId?.groupId == groupData.group.groupId)) {
context.read<BoardBloc>().add(
BoardEvent.createRow(
groupData.group.groupId,
value.position == CreateBoardCardRelativePosition.before
? OrderObjectPositionTypePB.Before
: OrderObjectPositionTypePB.After,
null,
widget.groupItem.row.id,
),
);
}
},
orElse: () {},
);
},
child: Shortcuts(
shortcuts: {
const SingleActivator(LogicalKeyboardKey.arrowUp): nada,
const SingleActivator(LogicalKeyboardKey.arrowDown): nada,
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
nada,
const SingleActivator(LogicalKeyboardKey.keyE): nada,
const SingleActivator(LogicalKeyboardKey.keyN): nada,
const SingleActivator(LogicalKeyboardKey.delete): nada,
const SingleActivator(LogicalKeyboardKey.backspace): nada,
const SingleActivator(LogicalKeyboardKey.enter): nada,
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
const SingleActivator(LogicalKeyboardKey.comma): nada,
const SingleActivator(LogicalKeyboardKey.period): nada,
SingleActivator(
LogicalKeyboardKey.arrowUp,
shift: true,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): nada,
},
child: ConditionalListenableBuilder<List<GroupedRowId>>(
valueListenable: widget.notifier,
buildWhen: (previous, current) {
final focusItem = GroupedRowId(
groupId: groupData.group.groupId,
rowId: rowMeta.id,
);
final previousContainsFocus = previous.contains(focusItem);
final currentContainsFocus = current.contains(focusItem);
return Container(
key: ValueKey(groupItemId),
margin: config.cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard(
fieldController: databaseController.fieldController,
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: isEditing,
cellBuilder: cellBuilder,
openCard: (context) => _openCard(
context: context,
databaseController: databaseController,
groupId: groupData.group.groupId,
rowMeta: context.read<CardBloc>().state.rowMeta,
),
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: desktopBoardCardCellStyleMap(context),
hoverStyle: HoverStyle(
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
: const Color(0x0FEFF4FB),
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
return previousContainsFocus != currentContainsFocus;
},
builder: (context, focusedItems, child) => Container(
margin: widget.boardConfig.cardMargin,
decoration: _makeBoxDecoration(
context,
groupData.group.groupId,
widget.groupItem.id,
),
child: child,
),
child: RowCard(
fieldController: databaseController.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: rowCache,
groupingFieldId: widget.groupItem.fieldInfo.id,
isEditing: _isEditing,
cellBuilder: widget.cellBuilder,
onTap: (context) => _openCard(
context: context,
databaseController: databaseController,
rowMeta: context.read<CardBloc>().state.rowMeta,
),
onShiftTap: (_) {
Focus.of(context).requestFocus();
widget.notifier.toggle(
GroupedRowId(
rowId: widget.groupItem.row.id,
groupId: groupData.group.groupId,
),
);
},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: desktopBoardCardCellStyleMap(context),
hoverStyle: HoverStyle(
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
: const Color(0x0FEFF4FB),
foregroundColorOnHover:
Theme.of(context).colorScheme.onBackground,
),
),
onStartEditing: () =>
context.read<BoardActionsCubit>().startEditingRow(
GroupedRowId(
groupId: groupData.group.groupId,
rowId: rowMeta.id,
),
),
onEndEditing: () => context.read<BoardActionsCubit>().endEditing(
GroupedRowId(
groupId: groupData.group.groupId,
rowId: rowMeta.id,
),
),
),
),
onStartEditing: () =>
boardBloc.add(BoardEvent.startEditingRow(groupData.group, rowMeta)),
onEndEditing: () => boardBloc.add(BoardEvent.endEditingRow(rowMeta.id)),
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context) {
BoxDecoration _makeBoxDecoration(
BuildContext context,
String groupId,
String rowId,
) {
return BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xFF1F2329).withOpacity(0.12)
: const Color(0xFF59647A),
color: widget.notifier
.isFocused(GroupedRowId(rowId: rowId, groupId: groupId))
? Theme.of(context).colorScheme.primary
: Theme.of(context).brightness == Brightness.light
? const Color(0xFF1F2329).withOpacity(0.12)
: const Color(0xFF59647A),
),
),
boxShadow: [
@@ -314,39 +724,6 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
],
);
}
void _openCard({
required BuildContext context,
required DatabaseController databaseController,
required String groupId,
required RowMetaPB rowMeta,
}) {
final rowInfo = RowInfo(
viewId: databaseController.viewId,
fields:
UnmodifiableListView(databaseController.fieldController.fieldInfos),
rowMeta: rowMeta,
rowId: rowMeta.id,
);
final rowController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: rowInfo.viewId,
rowCache: databaseController.rowCache,
groupId: groupId,
);
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
),
);
}
}
class BoardTrailing extends StatefulWidget {
@@ -458,3 +835,23 @@ class _BoardTrailingState extends State<BoardTrailing> {
}
}
}
void _openCard({
required BuildContext context,
required DatabaseController databaseController,
required RowMetaPB rowMeta,
}) {
final rowController = RowController(
rowMeta: rowMeta,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
);
}
@@ -4,8 +4,7 @@ import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -65,72 +64,83 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
if (state.isEditingHeader) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
if (state.editingHeaderId != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
Widget title = Expanded(
child: FlowyText.medium(
widget.groupData.headerData.groupName,
overflow: TextOverflow.ellipsis,
),
);
Widget title = Expanded(
child: FlowyText.medium(
widget.groupData.headerData.groupName,
overflow: TextOverflow.ellipsis,
),
);
if (!boardCustomData.group.isDefault &&
boardCustomData.fieldType.canEditHeader) {
title = Flexible(
fit: FlexFit.tight,
child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context
.read<BoardBloc>()
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
child: FlowyText.medium(
widget.groupData.headerData.groupName,
overflow: TextOverflow.ellipsis,
if (!boardCustomData.group.isDefault &&
boardCustomData.fieldType.canEditHeader) {
title = Flexible(
fit: FlexFit.tight,
child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context.read<BoardBloc>().add(
BoardEvent.startEditingHeader(widget.groupData.id),
),
child: FlowyText.medium(
widget.groupData.headerData.groupName,
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}
if (state.editingHeaderId == widget.groupData.id) {
title = _buildTextField(context);
}
return Padding(
padding: widget.margin,
child: SizedBox(
height: 50,
child: Row(
children: [
_buildHeaderIcon(boardCustomData),
title,
const HSpace(6),
_groupOptionsButton(context),
const HSpace(4),
FlowyTooltip(
message:
LocaleKeys.board_column_addToColumnTopTooltip.tr(),
preferBelow: false,
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover:
Theme.of(context).colorScheme.onSurface,
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
widget.groupData.id,
OrderObjectPositionTypePB.Start,
null,
null,
),
),
),
),
],
),
),
),
);
}
if (state.isEditingHeader &&
state.editingHeaderId == widget.groupData.id) {
title = _buildTextField(context);
}
return Padding(
padding: widget.margin,
child: SizedBox(
height: 50,
child: Row(
children: [
_buildHeaderIcon(boardCustomData),
title,
const HSpace(6),
_groupOptionsButton(context),
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
preferBelow: false,
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
),
),
],
),
),
);
},
);
},
);
@@ -257,7 +267,7 @@ enum GroupOptions {
case hide:
context
.read<BoardBloc>()
.add(BoardEvent.toggleGroupVisibility(group, false));
.add(BoardEvent.setGroupVisibility(group, false));
break;
case delete:
NavigatorAlertDialog(
@@ -0,0 +1,365 @@
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
class BoardFocusScope extends ChangeNotifier
implements ValueListenable<List<GroupedRowId>> {
BoardFocusScope({
required this.boardController,
});
final AppFlowyBoardController boardController;
List<GroupedRowId> _focusedCards = [];
@override
List<GroupedRowId> get value => _focusedCards;
UnmodifiableListView<GroupedRowId> get focusedGroupedRows =>
UnmodifiableListView(_focusedCards);
set focusedGroupedRows(List<GroupedRowId> focusedGroupedRows) {
_deepCopy();
_focusedCards
..clear()
..addAll(focusedGroupedRows);
notifyListeners();
}
bool isFocused(GroupedRowId groupedRowId) =>
_focusedCards.contains(groupedRowId);
void toggle(GroupedRowId groupedRowId) {
_deepCopy();
if (_focusedCards.contains(groupedRowId)) {
_focusedCards.remove(groupedRowId);
} else {
_focusedCards.add(groupedRowId);
}
notifyListeners();
}
void focusNext() {
_deepCopy();
// if no card is focused, focus on the first card in the board
if (_focusedCards.isEmpty) {
_focusFirstCard();
notifyListeners();
return;
}
final lastFocusedCard = _focusedCards.last;
final groupController = boardController.controller(lastFocusedCard.groupId);
final iterable = groupController?.items
.skipWhile((item) => item.id != lastFocusedCard.rowId);
// if the last-focused card's group cannot be found, or if the last-focused card cannot be found in the group, focus on the first card in the board
if (iterable == null || iterable.isEmpty) {
_focusFirstCard();
notifyListeners();
return;
}
if (iterable.length == 1) {
// focus on the first card in the next group
final group = boardController.groupDatas
.skipWhile((item) => item.id != lastFocusedCard.groupId)
.skip(1)
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
if (group != null) {
_focusedCards
..clear()
..add(
GroupedRowId(
rowId: group.items.first.id,
groupId: group.id,
),
);
}
} else {
// focus on the next card in the same group
_focusedCards
..clear()
..add(
GroupedRowId(
rowId: iterable.elementAt(1).id,
groupId: lastFocusedCard.groupId,
),
);
}
notifyListeners();
}
void focusPrevious() {
_deepCopy();
// if no card is focused, focus on the last card in the board
if (_focusedCards.isEmpty) {
_focusLastCard();
notifyListeners();
return;
}
final lastFocusedCard = _focusedCards.last;
final groupController = boardController.controller(lastFocusedCard.groupId);
final iterable = groupController?.items.reversed
.skipWhile((item) => item.id != lastFocusedCard.rowId);
// if the last-focused card's group cannot be found or if the last-focused card cannot be found in the group, focus on the last card in the board
if (iterable == null || iterable.isEmpty) {
_focusLastCard();
notifyListeners();
return;
}
if (iterable.length == 1) {
// focus on the last card in the previous group
final group = boardController.groupDatas.reversed
.skipWhile((item) => item.id != lastFocusedCard.groupId)
.skip(1)
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
if (group != null) {
_focusedCards
..clear()
..add(
GroupedRowId(
rowId: group.items.last.id,
groupId: group.id,
),
);
}
} else {
// focus on the next card in the same group
_focusedCards
..clear()
..add(
GroupedRowId(
rowId: iterable.elementAt(1).id,
groupId: lastFocusedCard.groupId,
),
);
}
notifyListeners();
}
void adjustRangeDown() {
_deepCopy();
// if no card is focused, focus on the first card in the board
if (_focusedCards.isEmpty) {
_focusFirstCard();
notifyListeners();
return;
}
final firstFocusedCard = _focusedCards.first;
final lastFocusedCard = _focusedCards.last;
// determine whether to shrink or expand the selection
bool isExpand = false;
if (_focusedCards.length == 1) {
isExpand = true;
} else {
final firstGroupIndex = boardController.groupDatas
.indexWhere((element) => element.id == firstFocusedCard.groupId);
final lastGroupIndex = boardController.groupDatas
.indexWhere((element) => element.id == lastFocusedCard.groupId);
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
_focusFirstCard();
notifyListeners();
return;
}
if (firstGroupIndex < lastGroupIndex) {
isExpand = true;
} else if (firstGroupIndex > lastGroupIndex) {
isExpand = false;
} else {
final groupItems =
boardController.groupDatas.elementAt(firstGroupIndex).items;
final firstCardIndex =
groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);
final lastCardIndex =
groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);
if (firstCardIndex == -1 || lastCardIndex == -1) {
_focusFirstCard();
notifyListeners();
return;
}
isExpand = firstCardIndex < lastCardIndex;
}
}
if (isExpand) {
final groupController =
boardController.controller(lastFocusedCard.groupId);
if (groupController == null) {
_focusFirstCard();
notifyListeners();
return;
}
final iterable = groupController.items
.skipWhile((item) => item.id != lastFocusedCard.rowId);
if (iterable.length == 1) {
// focus on the first card in the next group
final group = boardController.groupDatas
.skipWhile((item) => item.id != lastFocusedCard.groupId)
.skip(1)
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
if (group != null) {
_focusedCards.add(
GroupedRowId(
rowId: group.items.first.id,
groupId: group.id,
),
);
}
} else {
_focusedCards.add(
GroupedRowId(
rowId: iterable.elementAt(1).id,
groupId: lastFocusedCard.groupId,
),
);
}
} else {
_focusedCards.removeLast();
}
notifyListeners();
}
void adjustRangeUp() {
_deepCopy();
// if no card is focused, focus on the first card in the board
if (_focusedCards.isEmpty) {
_focusLastCard();
notifyListeners();
return;
}
final firstFocusedCard = _focusedCards.first;
final lastFocusedCard = _focusedCards.last;
// determine whether to shrink or expand the selection
bool isExpand = false;
if (_focusedCards.length == 1) {
isExpand = true;
} else {
final firstGroupIndex = boardController.groupDatas
.indexWhere((element) => element.id == firstFocusedCard.groupId);
final lastGroupIndex = boardController.groupDatas
.indexWhere((element) => element.id == lastFocusedCard.groupId);
if (firstGroupIndex == -1 || lastGroupIndex == -1) {
_focusLastCard();
notifyListeners();
return;
}
if (firstGroupIndex < lastGroupIndex) {
isExpand = false;
} else if (firstGroupIndex > lastGroupIndex) {
isExpand = true;
} else {
final groupItems =
boardController.groupDatas.elementAt(firstGroupIndex).items;
final firstCardIndex =
groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);
final lastCardIndex =
groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);
if (firstCardIndex == -1 || lastCardIndex == -1) {
_focusLastCard();
notifyListeners();
return;
}
isExpand = firstCardIndex > lastCardIndex;
}
}
if (isExpand) {
final groupController =
boardController.controller(lastFocusedCard.groupId);
if (groupController == null) {
_focusLastCard();
notifyListeners();
return;
}
final iterable = groupController.items.reversed
.skipWhile((item) => item.id != lastFocusedCard.rowId);
if (iterable.length == 1) {
// focus on the last card in the previous group
final group = boardController.groupDatas.reversed
.skipWhile((item) => item.id != lastFocusedCard.groupId)
.skip(1)
.firstWhereOrNull((groupData) => groupData.items.isNotEmpty);
if (group != null) {
_focusedCards.add(
GroupedRowId(
rowId: group.items.last.id,
groupId: group.id,
),
);
}
} else {
_focusedCards.add(
GroupedRowId(
rowId: iterable.elementAt(1).id,
groupId: lastFocusedCard.groupId,
),
);
}
} else {
_focusedCards.removeLast();
}
notifyListeners();
}
void clear() {
_deepCopy();
_focusedCards.clear();
notifyListeners();
}
void _focusFirstCard() {
_focusedCards.clear();
final firstGroup = boardController.groupDatas
.firstWhereOrNull((group) => group.items.isNotEmpty);
final firstCard = firstGroup?.items.firstOrNull;
if (firstCard != null) {
_focusedCards
.add(GroupedRowId(rowId: firstCard.id, groupId: firstGroup!.id));
}
}
void _focusLastCard() {
_focusedCards.clear();
final lastGroup = boardController.groupDatas
.lastWhereOrNull((group) => group.items.isNotEmpty);
final lastCard = lastGroup?.items.lastOrNull;
if (lastCard != null) {
_focusedCards
.add(GroupedRowId(rowId: lastCard.id, groupId: lastGroup!.id));
}
}
void _deepCopy() {
_focusedCards = [..._focusedCards];
}
}
@@ -11,7 +11,6 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
@@ -23,7 +22,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HiddenGroupsColumn extends StatelessWidget {
const HiddenGroupsColumn({super.key, required this.margin});
const HiddenGroupsColumn({
super.key,
required this.margin,
});
final EdgeInsets margin;
@@ -31,7 +33,10 @@ class HiddenGroupsColumn extends StatelessWidget {
Widget build(BuildContext context) {
final databaseController = context.read<BoardBloc>().databaseController;
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
selector: (state) => state.layoutSettings,
selector: (state) => state.maybeMap(
orElse: () => null,
ready: (value) => value.layoutSettings,
),
builder: (context, layoutSettings) {
if (layoutSettings == null) {
return const SizedBox.shrink();
@@ -126,43 +131,48 @@ class HiddenGroupList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [
child,
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
builder: (context, state) {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) => ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [
child,
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
),
],
),
],
),
buildDefaultDragHandles: false,
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.only(bottom: 4),
key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"),
child: HiddenGroupCard(
group: state.hiddenGroups[index],
index: index,
bloc: context.read<BoardBloc>(),
),
),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
),
buildDefaultDragHandles: false,
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.only(bottom: 4),
key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"),
child: HiddenGroupCard(
group: state.hiddenGroups[index],
index: index,
bloc: context.read<BoardBloc>(),
),
),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
},
);
}
}
@@ -248,57 +258,63 @@ class HiddenGroupButtonContent extends StatelessWidget {
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return SizedBox(
height: 32,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 3,
),
child: Row(
children: [
HiddenGroupCardActions(
isVisible: isHovering,
index: index,
return SizedBox(
height: 32,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 3,
),
const HSpace(4),
FlowyText.medium(
bloc.generateGroupNameFromGroup(group),
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
Expanded(
child: FlowyText.medium(
group.rows.length.toString(),
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
if (isHovering) ...[
FlowyIconButton(
width: 20,
icon: FlowySvg(
FlowySvgs.show_m,
color: Theme.of(context).hintColor,
child: Row(
children: [
HiddenGroupCardActions(
isVisible: isHovering,
index: index,
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
const HSpace(4),
FlowyText.medium(
bloc.generateGroupNameFromGroup(group),
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
Expanded(
child: FlowyText.medium(
group.rows.length.toString(),
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
if (isHovering) ...[
FlowyIconButton(
width: 20,
icon: FlowySvg(
FlowySvgs.show_m,
color: Theme.of(context).hintColor,
),
),
],
],
),
),
onPressed: () =>
context.read<BoardBloc>().add(
BoardEvent.setGroupVisibility(
group,
true,
),
),
),
],
],
),
),
);
},
);
},
),
@@ -360,68 +376,71 @@ class HiddenGroupPopupItemList extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
context.read<BoardBloc>().generateGroupNameFromGroup(group),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final databaseController =
context.read<BoardBloc>().databaseController;
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item).firstWhere(
(cellContext) => cellContext.fieldId == primaryFieldId,
),
rowController: rowController,
rowMeta: item,
cellBuilder: CardCellBuilder(
databaseController: databaseController,
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
context.read<BoardBloc>().generateGroupNameFromGroup(group),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final databaseController =
context.read<BoardBloc>().databaseController;
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item).firstWhere(
(cellContext) =>
cellContext.fieldId == primaryFieldId,
),
rowController: rowController,
rowMeta: item,
cellBuilder: CardCellBuilder(
databaseController: databaseController,
),
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) {
return RowDetailPage(
databaseController: databaseController,
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
PopoverContainer.of(context).close();
},
);
},
),
];
),
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
);
},
);
@@ -0,0 +1,146 @@
import 'dart:io';
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'board_focus_scope.dart';
class BoardShortcutContainer extends StatelessWidget {
const BoardShortcutContainer({
super.key,
required this.focusScope,
required this.child,
});
final BoardFocusScope focusScope;
final Widget child;
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.arrowUp):
focusScope.focusPrevious,
const SingleActivator(LogicalKeyboardKey.arrowDown):
focusScope.focusNext,
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true):
focusScope.adjustRangeUp,
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):
focusScope.adjustRangeDown,
const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear,
const SingleActivator(LogicalKeyboardKey.keyE): () {
if (focusScope.value.length != 1) {
return;
}
context
.read<BoardActionsCubit>()
.startEditingRow(focusScope.value.first);
},
const SingleActivator(LogicalKeyboardKey.keyN): () {
if (focusScope.value.length != 1) {
return;
}
context
.read<BoardActionsCubit>()
.startCreateBottomRow(focusScope.value.first.groupId);
focusScope.clear();
},
const SingleActivator(LogicalKeyboardKey.delete): () =>
_removeHandler(context),
const SingleActivator(LogicalKeyboardKey.backspace): () =>
_removeHandler(context),
SingleActivator(
LogicalKeyboardKey.arrowUp,
shift: true,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): () => _shiftCmdUpHandler(context),
const SingleActivator(LogicalKeyboardKey.enter): () =>
_enterHandler(context),
const SingleActivator(LogicalKeyboardKey.numpadEnter): () =>
_enterHandler(context),
const SingleActivator(LogicalKeyboardKey.enter, shift: true): () =>
_shitEnterHandler(context),
const SingleActivator(LogicalKeyboardKey.comma): () =>
_moveGroupToAdjacentGroup(context, true),
const SingleActivator(LogicalKeyboardKey.period): () =>
_moveGroupToAdjacentGroup(context, false),
},
child: FocusScope(
child: Focus(
child: Builder(
builder: (context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
final focusNode = Focus.of(context);
focusNode.requestFocus();
focusScope.clear();
},
child: child,
);
},
),
),
),
);
}
void _enterHandler(BuildContext context) {
if (focusScope.value.length != 1) {
return;
}
context
.read<BoardActionsCubit>()
.openCardWithRowId(focusScope.value.first.rowId);
}
void _shitEnterHandler(BuildContext context) {
if (focusScope.value.isEmpty) {
context
.read<BoardActionsCubit>()
.createRow(null, CreateBoardCardRelativePosition.after);
} else if (focusScope.value.length == 1) {
context.read<BoardActionsCubit>().createRow(
focusScope.value.first,
CreateBoardCardRelativePosition.after,
);
}
}
void _shiftCmdUpHandler(BuildContext context) {
if (focusScope.value.isEmpty) {
context
.read<BoardActionsCubit>()
.createRow(null, CreateBoardCardRelativePosition.before);
} else if (focusScope.value.length == 1) {
context.read<BoardActionsCubit>().createRow(
focusScope.value.first,
CreateBoardCardRelativePosition.before,
);
}
}
void _removeHandler(BuildContext context) {
if (focusScope.value.isEmpty) {
return;
}
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
}
void _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) {
if (focusScope.value.length != 1) {
return;
}
context.read<BoardBloc>().add(
BoardEvent.moveGroupToAdjacentGroup(
focusScope.value.first,
toPrevious,
),
);
focusScope.clear();
}
}
@@ -72,7 +72,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
},
deleteEvent: (String viewId, String rowId) async {
final result = await RowBackendService.deleteRow(viewId, rowId);
final result = await RowBackendService.deleteRows(viewId, [rowId]);
result.fold(
(_) => null,
(e) => Log.error('Failed to delete event: $e', e),
@@ -46,9 +46,9 @@ class CalendarEventEditorBloc
emit(state.copyWith(cells: cells));
},
delete: () async {
final result = await RowBackendService.deleteRow(
final result = await RowBackendService.deleteRows(
rowController.viewId,
rowController.rowId,
[rowController.rowId],
);
result.fold((l) => null, (err) => Log.error(err));
},
@@ -80,7 +80,7 @@ class _EventCardState extends State<EventCard> {
rowCache: rowCache,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) {
onTap: (context) {
if (PlatformExtension.isMobile) {
context.push(
MobileRowDetailPage.routeName,
@@ -51,7 +51,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
emit(state.copyWith(createdRow: null, openRowDetail: false));
},
deleteRow: (rowInfo) async {
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
await RowBackendService.deleteRows(viewId, [rowInfo.rowId]);
},
moveRow: (int from, int to) {
final List<RowInfo> rows = [...state.rowInfos];
@@ -102,7 +102,7 @@ enum RowAction {
RowBackendService.duplicateRow(viewId, rowId);
break;
case delete:
RowBackendService.deleteRow(viewId, rowId);
RowBackendService.deleteRows(viewId, [rowId]);
break;
}
}
@@ -1,6 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart';
@@ -29,10 +28,11 @@ class RowCard extends StatefulWidget {
required this.isEditing,
required this.rowCache,
required this.cellBuilder,
required this.openCard,
required this.onTap,
required this.onStartEditing,
required this.onEndEditing,
required this.styleConfiguration,
this.onShiftTap,
this.groupingFieldId,
this.groupId,
});
@@ -50,7 +50,9 @@ class RowCard extends StatefulWidget {
final CardCellBuilder cellBuilder;
/// Called when the user taps on the card.
final void Function(BuildContext) openCard;
final void Function(BuildContext context) onTap;
final void Function(BuildContext context)? onShiftTap;
/// Called when the user starts editing the card.
final VoidCallback onStartEditing;
@@ -67,12 +69,10 @@ class RowCard extends StatefulWidget {
class _RowCardState extends State<RowCard> {
final popoverController = PopoverController();
late final CardBloc _cardBloc;
late final EditableRowNotifier rowNotifier;
@override
void initState() {
super.initState();
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
_cardBloc = CardBloc(
fieldController: widget.fieldController,
viewId: widget.viewId,
@@ -81,22 +81,18 @@ class _RowCardState extends State<RowCard> {
rowMeta: widget.rowMeta,
rowCache: widget.rowCache,
)..add(const CardEvent.initial());
}
rowNotifier.isEditing.addListener(() {
if (!mounted) return;
_cardBloc.add(CardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) {
widget.onStartEditing();
} else {
widget.onEndEditing();
}
});
@override
void didUpdateWidget(covariant oldWidget) {
if (widget.isEditing != _cardBloc.state.isEditing) {
_cardBloc.add(CardEvent.setIsEditing(widget.isEditing));
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
@@ -105,7 +101,14 @@ class _RowCardState extends State<RowCard> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<CardBloc, CardState>(
child: BlocConsumer<CardBloc, CardState>(
listenWhen: (previous, current) =>
previous.isEditing != current.isEditing,
listener: (context, state) {
if (!state.isEditing) {
widget.onEndEditing();
}
},
builder: (context, state) =>
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
),
@@ -114,7 +117,7 @@ class _RowCardState extends State<RowCard> {
Widget _mobile(CardState state) {
return GestureDetector(
onTap: () => widget.openCard(context),
onTap: () => widget.onTap(context),
behavior: HitTestBehavior.opaque,
child: MobileCardContent(
rowMeta: state.rowMeta,
@@ -127,9 +130,9 @@ class _RowCardState extends State<RowCard> {
Widget _desktop(CardState state) {
final accessories = widget.styleConfiguration.showAccessory
? <CardAccessory>[
EditCardAccessory(rowNotifier: rowNotifier),
const MoreCardOptionsAccessory(),
? const <CardAccessory>[
EditCardAccessory(),
MoreCardOptionsAccessory(),
]
: null;
return AppFlowyPopover(
@@ -148,10 +151,10 @@ class _RowCardState extends State<RowCard> {
buildAccessoryWhen: () => state.isEditing == false,
accessories: accessories ?? [],
openAccessory: _handleOpenAccessory,
openCard: widget.openCard,
onTap: widget.onTap,
onShiftTap: widget.onShiftTap,
child: _CardContent(
rowMeta: state.rowMeta,
rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
@@ -163,6 +166,7 @@ class _RowCardState extends State<RowCard> {
void _handleOpenAccessory(AccessoryType newAccessoryType) {
switch (newAccessoryType) {
case AccessoryType.edit:
widget.onStartEditing();
break;
case AccessoryType.more:
popoverController.show();
@@ -174,16 +178,14 @@ class _RowCardState extends State<RowCard> {
class _CardContent extends StatelessWidget {
const _CardContent({
required this.rowMeta,
required this.rowNotifier,
required this.cellBuilder,
required this.cells,
required this.styleConfiguration,
});
final RowMetaPB rowMeta;
final EditableRowNotifier rowNotifier;
final CardCellBuilder cellBuilder;
final List<CellContext> cells;
final List<CellMeta> cells;
final RowCardStyleConfiguration styleConfiguration;
@override
@@ -199,7 +201,7 @@ class _CardContent extends StatelessWidget {
? child
: FlowyHover(
style: styleConfiguration.hoverStyle,
buildWhenOnHover: () => !rowNotifier.isEditing.value,
buildWhenOnHover: () => !context.read<CardBloc>().state.isEditing,
child: child,
);
}
@@ -207,22 +209,22 @@ class _CardContent extends StatelessWidget {
List<Widget> _makeCells(
BuildContext context,
RowMetaPB rowMeta,
List<CellContext> cells,
List<CellMeta> cells,
) {
// Remove all the cell listeners.
rowNotifier.unbind();
return cells.mapIndexed((int index, CellContext cellContext) {
return cells.mapIndexed((int index, CellMeta cellMeta) {
EditableCardNotifier? cellNotifier;
if (index == 0) {
cellNotifier =
EditableCardNotifier(isEditing: rowNotifier.isEditing.value);
rowNotifier.bindCell(cellContext, cellNotifier);
final bloc = context.read<CardBloc>();
cellNotifier = EditableCardNotifier(isEditing: bloc.state.isEditing);
cellNotifier.isCellEditing.addListener(() {
final isEditing = cellNotifier!.isCellEditing.value;
bloc.add(CardEvent.setIsEditing(isEditing));
});
}
return cellBuilder.build(
cellContext: cellContext,
cellContext: cellMeta.cellContext(),
cellNotifier: cellNotifier,
styleMap: styleConfiguration.cellStyleMap,
hasNotes: !rowMeta.isDocumentEmpty,
@@ -231,6 +233,24 @@ class _CardContent extends StatelessWidget {
}
}
class EditCardAccessory extends StatelessWidget with CardAccessory {
const EditCardAccessory({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(3.0),
child: FlowySvg(
FlowySvgs.edit_s,
color: Theme.of(context).hintColor,
),
);
}
@override
AccessoryType get type => AccessoryType.edit;
}
class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
const MoreCardOptionsAccessory({super.key});
@@ -249,29 +269,6 @@ class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
AccessoryType get type => AccessoryType.more;
}
class EditCardAccessory extends StatelessWidget with CardAccessory {
const EditCardAccessory({super.key, required this.rowNotifier});
final EditableRowNotifier rowNotifier;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(3.0),
child: FlowySvg(
FlowySvgs.edit_s,
color: Theme.of(context).hintColor,
),
);
}
@override
void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
@override
AccessoryType get type => AccessoryType.edit;
}
class RowCardStyleConfiguration {
const RowCardStyleConfiguration({
required this.cellStyleMap,
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@@ -7,7 +9,6 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/domain/row_listener.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -104,7 +105,7 @@ class CardBloc extends Bloc<CardEvent, CardState> {
}
}
List<CellContext> _makeCells(
List<CellMeta> _makeCells(
FieldController fieldController,
String? groupFieldId,
List<CellContext> cellContexts,
@@ -116,7 +117,15 @@ List<CellContext> _makeCells(
!(fieldInfo.visibility?.isVisibleState() ?? false) ||
(groupFieldId != null && cellContext.fieldId == groupFieldId);
});
return cellContexts.toList();
return cellContexts
.map(
(cellCtx) => CellMeta(
fieldId: cellCtx.fieldId,
rowId: cellCtx.rowId,
fieldType: fieldController.getField(cellCtx.fieldId)!.fieldType,
),
)
.toList();
}
@freezed
@@ -124,17 +133,30 @@ class CardEvent with _$CardEvent {
const factory CardEvent.initial() = _InitialRow;
const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory CardEvent.didReceiveCells(
List<CellContext> cells,
List<CellMeta> cells,
ChangedReason reason,
) = _DidReceiveCells;
const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) =
_DidUpdateRowMeta;
}
@freezed
class CellMeta with _$CellMeta {
const CellMeta._();
const factory CellMeta({
required String fieldId,
required RowId rowId,
required FieldType fieldType,
}) = _DatabaseCellMeta;
CellContext cellContext() => CellContext(fieldId: fieldId, rowId: rowId);
}
@freezed
class CardState with _$CardState {
const factory CardState({
required List<CellContext> cells,
required List<CellMeta> cells,
required RowMetaPB rowMeta,
required bool isEditing,
ChangedReason? changeReason,
@@ -142,7 +164,7 @@ class CardState with _$CardState {
factory CardState.initial(
RowMetaPB rowMeta,
List<CellContext> cells,
List<CellMeta> cells,
bool isEditing,
) =>
CardState(
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'accessory.dart';
@@ -7,14 +8,16 @@ class RowCardContainer extends StatelessWidget {
const RowCardContainer({
super.key,
required this.child,
required this.openCard,
required this.onTap,
required this.openAccessory,
required this.accessories,
this.buildAccessoryWhen,
this.onShiftTap,
});
final Widget child;
final void Function(BuildContext) openCard;
final void Function(BuildContext) onTap;
final void Function(BuildContext)? onShiftTap;
final void Function(AccessoryType) openAccessory;
final List<CardAccessory> accessories;
final bool Function()? buildAccessoryWhen;
@@ -41,7 +44,13 @@ class RowCardContainer extends StatelessWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => openCard(context),
onTap: () {
if (HardwareKeyboard.instance.isShiftPressed) {
onShiftTap?.call(context);
} else {
onTap(context);
}
},
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: container,
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:flutter/material.dart';
abstract class CardCell<T extends CardCellStyle> extends StatefulWidget {
@@ -32,61 +31,6 @@ class EditableCardNotifier {
}
}
class EditableRowNotifier {
EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing);
final Map<CellContext, EditableCardNotifier> _cells = {};
final ValueNotifier<bool> isEditing;
void bindCell(
CellContext cellIdentifier,
EditableCardNotifier notifier,
) {
assert(
_cells.values.isEmpty,
'Only one cell can receive the notification',
);
_cells[cellIdentifier]?.dispose();
notifier.isCellEditing.addListener(() {
isEditing.value = notifier.isCellEditing.value;
});
_cells[cellIdentifier] = notifier;
}
void becomeFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = true;
}
void resignFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = false;
}
void unbind() {
for (final notifier in _cells.values) {
notifier.dispose();
}
_cells.clear();
}
void dispose() {
unbind();
isEditing.dispose();
}
}
abstract mixin class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
@@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_builder.dart';
@@ -54,18 +55,26 @@ class _TextCellState extends State<TextCardCell> {
widget.cellContext,
).as(),
);
late final TextEditingController _textEditingController =
TextEditingController(text: cellBloc.state.content);
late final TextEditingController _textEditingController;
final focusNode = SingleListenerFocusNode();
bool focusWhenInit = false;
@override
void initState() {
super.initState();
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
focusNode.requestFocus();
_textEditingController = TextEditingController(text: cellBloc.state.content)
..addListener(() {
if (_textEditingController.value.composing.isCollapsed) {
cellBloc
.add(TextCellEvent.updateText(_textEditingController.value.text));
}
});
if (widget.editableNotifier?.isCellEditing.value ?? false) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
cellBloc.add(const TextCellEvent.enableEdit(true));
});
}
// If the focusNode lost its focus, the widget's editableNotifier will
@@ -73,7 +82,6 @@ class _TextCellState extends State<TextCardCell> {
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
cellBloc.add(const TextCellEvent.enableEdit(false));
}
@@ -99,52 +107,25 @@ class _TextCellState extends State<TextCardCell> {
@override
void didUpdateWidget(covariant oldWidget) {
_bindEditableNotifier();
if (oldWidget.editableNotifier != widget.editableNotifier) {
_bindEditableNotifier();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
return BlocProvider.value(
value: cellBloc,
child: BlocConsumer<TextCellBloc, TextCellState>(
listenWhen: (previous, current) =>
previous.content != current.content && !current.enableEdit,
child: BlocListener<TextCellBloc, TextCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
_textEditingController.text = state.content;
},
buildWhen: (previous, current) {
if (previous.content != current.content &&
_textEditingController.text == current.content) {
return false;
if (!state.enableEdit) {
_textEditingController.text = state.content;
}
return previous != current;
},
builder: (context, state) {
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false &&
!isTitle) {
return const SizedBox.shrink();
}
final icon = isTitle ? _buildIcon(state) : null;
final child = isTitle
? _buildTextField(state.enableEdit || focusWhenInit)
: _buildText(state.content);
return Row(
children: [
if (icon != null) ...[
icon,
const HSpace(4.0),
],
Expanded(child: child),
],
);
},
child: isTitle ? _buildTitle() : _buildText(),
),
);
}
@@ -152,6 +133,8 @@ class _TextCellState extends State<TextCardCell> {
@override
void dispose() {
_textEditingController.dispose();
widget.editableNotifier?.isCellEditing
.removeListener(_bindEditableNotifier);
focusNode.dispose();
cellBloc.close();
super.dispose();
@@ -176,53 +159,82 @@ class _TextCellState extends State<TextCardCell> {
return null;
}
Widget _buildText(String content) {
final text =
content.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : content;
final color = content.isEmpty ? Theme.of(context).hintColor : null;
Widget _buildText() {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final content = state.content;
return Padding(
padding: widget.style.padding,
child: Text(
text,
style: widget.style.textStyle.copyWith(color: color),
maxLines: widget.style.maxLines,
),
return content.isEmpty
? const SizedBox.shrink()
: Container(
padding: widget.style.padding,
alignment: AlignmentDirectional.centerStart,
child: Text(
content,
style: widget.style.textStyle,
maxLines: widget.style.maxLines,
),
);
},
);
}
Widget _buildTextField(bool isEditing) {
final padding =
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
return IgnorePointer(
ignoring: !isEditing,
child: TextField(
controller: _textEditingController,
focusNode: focusNode,
onChanged: (_) {
if (_textEditingController.value.composing.isCollapsed) {
cellBloc.add(TextCellEvent.updateText(_textEditingController.text));
}
},
onEditingComplete: () => focusNode.unfocus(),
maxLines: isEditing ? null : 2,
minLines: 1,
textInputAction: TextInputAction.done,
readOnly: !isEditing,
enableInteractiveSelection: isEditing,
style: widget.style.titleTextStyle,
decoration: InputDecoration(
contentPadding: padding,
border: InputBorder.none,
enabledBorder: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
hintStyle: widget.style.titleTextStyle.copyWith(
color: Theme.of(context).hintColor,
Widget _buildTitle() {
final textField = _buildTextField();
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final icon = _buildIcon(state);
return Row(
children: [
if (icon != null) ...[
icon,
const HSpace(4.0),
],
Expanded(child: textField),
],
);
},
);
}
Widget _buildTextField() {
return BlocSelector<TextCellBloc, TextCellState, bool>(
selector: (state) => state.enableEdit,
builder: (context, isEditing) {
return IgnorePointer(
ignoring: !isEditing,
child: CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.escape): () =>
focusNode.unfocus(),
},
child: TextField(
controller: _textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
maxLines: isEditing ? null : 2,
minLines: 1,
textInputAction: TextInputAction.done,
readOnly: !isEditing,
enableInteractiveSelection: isEditing,
style: widget.style.titleTextStyle,
decoration: InputDecoration(
contentPadding: widget.style.padding
.add(const EdgeInsets.symmetric(vertical: 4.0)),
border: InputBorder.none,
enabledBorder: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
hintStyle: widget.style.titleTextStyle.copyWith(
color: Theme.of(context).hintColor,
),
),
onTapOutside: (_) {},
),
),
),
),
);
},
);
}
}
@@ -79,8 +79,11 @@ class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<NumberCellBloc, NumberCellState>(
listener: (context, state) =>
_textEditingController.text = state.content,
listener: (context, state) {
if (!focusNode.hasFocus) {
_textEditingController.text = state.content;
}
},
child: Builder(
builder: (context) {
return widget.skin.build(
@@ -80,7 +80,9 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
value: cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
_textEditingController.text = state.content;
if (!focusNode.hasFocus) {
_textEditingController.text = state.content;
}
},
child: Builder(
builder: (context) {
@@ -112,8 +112,10 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
child: BlocListener<URLCellBloc, URLCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
_textEditingController.value =
_textEditingController.value.copyWith(text: state.content);
if (!focusNode.hasFocus) {
_textEditingController.value =
_textEditingController.value.copyWith(text: state.content);
}
widget._cellDataNotifier.value = state.content;
},
child: widget.skin.build(
@@ -20,7 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Relation,
// FieldType.Summary,
FieldType.Summary,
];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
@@ -53,7 +53,7 @@ class RowDetailPageDeleteButton extends StatelessWidget {
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
leftIcon: const FlowySvg(FlowySvgs.trash_m),
onTap: () {
RowBackendService.deleteRow(viewId, rowId);
RowBackendService.deleteRows(viewId, [rowId]);
FlowyOverlay.pop(context);
},
),
@@ -288,7 +288,6 @@ class _TitleSkin extends IEditableTextCellSkin {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
autofocus: true,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),
decoration: InputDecoration(
@@ -54,12 +54,7 @@ class ViewTitleBarWithRow extends StatelessWidget {
return Visibility(
visible: maxWidth < constraints.maxWidth,
// if the width is too small, only show one view title bar without the ancestors
replacement: _ViewTitle(
key: ValueKey(state.ancestors.last),
view: state.ancestors.last,
maxTitleWidth: constraints.maxWidth - 50.0,
onUpdated: () {},
),
replacement: _buildRowName(),
child: Row(
// refresh the view title bar when the ancestors changed
key: ValueKey(state.ancestors.hashCode),
@@ -104,42 +99,39 @@ class ViewTitleBarWithRow extends StatelessWidget {
}
Widget _buildRowName() {
return BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
builder: (context, state) {
if (state.databaseController == null) {
return const SizedBox.shrink();
}
return _RowName(
cellBuilder: EditableCellBuilder(
databaseController: state.databaseController!,
),
primaryFieldId: state.fieldId!,
rowId: rowId,
);
},
return _RowName(
rowId: rowId,
);
}
}
class _RowName extends StatelessWidget {
const _RowName({
required this.cellBuilder,
required this.primaryFieldId,
required this.rowId,
});
final EditableCellBuilder cellBuilder;
final String primaryFieldId;
final String rowId;
@override
Widget build(BuildContext context) {
return cellBuilder.buildCustom(
CellContext(
fieldId: primaryFieldId,
rowId: rowId,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
return BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
builder: (context, state) {
if (state.databaseController == null) {
return const SizedBox.shrink();
}
final cellBuilder = EditableCellBuilder(
databaseController: state.databaseController!,
);
return cellBuilder.buildCustom(
CellContext(
fieldId: state.fieldId!,
rowId: rowId,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
);
},
);
}
}
@@ -220,12 +212,10 @@ enum _ViewTitleBehavior {
class _ViewTitle extends StatefulWidget {
const _ViewTitle({
super.key,
required this.view,
this.behavior = _ViewTitleBehavior.editable,
this.maxTitleWidth = 180,
required this.onUpdated,
});
}) : maxTitleWidth = 180;
final ViewPB view;
final _ViewTitleBehavior behavior;
@@ -20,7 +20,6 @@ import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -76,13 +75,15 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
StreamSubscription? _transactionSubscription;
final _updateSelectionDebounce = Debounce();
final _syncThrottle = Throttler(duration: const Duration(milliseconds: 500));
bool isClosing = false;
static const _syncDuration = Duration(milliseconds: 250);
final _updateSelectionDebounce = Debounce(duration: _syncDuration);
final _syncThrottle = Throttler(duration: _syncDuration);
// The conflict handle logic is not fully implemented yet
// use the syncTimer to force to reload the document state when the conflict happens.
Timer? _syncTimer;
bool _shouldSync = false;
bool get isLocalMode {
final userProfilePB = state.userProfilePB;
@@ -92,6 +93,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
@override
Future<void> close() async {
isClosing = true;
_updateSelectionDebounce.dispose();
_syncThrottle.dispose();
await _documentService.syncAwarenessStates(documentId: documentId);
await _documentListener.stop();
await _syncStateListener.stop();
await _viewListener?.stop();
@@ -110,7 +115,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
) async {
await event.when(
initial: () async {
_resetSyncTimer();
final result = await _fetchDocumentState();
_onViewChanged();
_onDocumentChanged();
@@ -205,19 +209,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
);
}
void _resetSyncTimer() {
_syncTimer?.cancel();
_syncTimer = null;
_syncTimer = Timer.periodic(const Duration(seconds: 10), (_) {
if (!_shouldSync) {
return;
}
Log.debug('auto sync document');
// unawaited(_documentCollabAdapter.forceReload());
_shouldSync = false;
});
}
/// Fetch document
Future<FlowyResult<EditorState?, FlowyError>> _fetchDocumentState() async {
final result = await _documentService.openDocument(documentId: documentId);
@@ -257,10 +248,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
// ignore: invalid_use_of_visible_for_testing_member
emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));
}
// reset the sync timer
_shouldSync = true;
_resetSyncTimer();
},
);
@@ -322,8 +309,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
unawaited(_documentCollabAdapter.syncV3(docEvent: docEvent));
_resetSyncTimer();
}
Future<void> _onAwarenessStatesUpdate(
@@ -347,13 +332,15 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
void _throttleSyncDoc(DocEventPB docEvent) {
_shouldSync = true;
_syncThrottle.call(() {
_onDocumentStateUpdate(docEvent);
});
}
Future<void> _onSelectionUpdate() async {
if (isClosing) {
return;
}
final user = state.userProfilePB;
final deviceId = ApplicationInfo.deviceId;
if (!FeatureFlag.syncDocument.isOn || user == null) {
@@ -183,7 +183,7 @@ class DocumentCollabAdapter {
for (final state in values) {
// the following code is only for version 1
if (state.version != 1 || state.metadata.isEmpty) {
return;
continue;
}
final uid = state.user.uid.toString();
final did = state.user.deviceId;
@@ -244,9 +244,8 @@ class DocumentCollabAdapter {
);
remoteSelections.add(remoteSelection);
}
if (remoteSelections.isNotEmpty) {
editorState.remoteSelections.value = remoteSelections;
}
editorState.remoteSelections.value = remoteSelections;
}
}
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
@@ -10,7 +11,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'
Delta,
ParagraphBlockKeys,
NodeIterator,
NodeExternalValues;
NodeExternalValues,
HeadingBlockKeys,
QuoteBlockKeys,
NumberedListBlockKeys,
BulletedListBlockKeys,
blockComponentDelta;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:collection/collection.dart';
import 'package:nanoid/nanoid.dart';
@@ -144,21 +151,25 @@ extension BlockToNode on BlockPB {
final deltaString = meta.textMap[externalId];
if (deltaString != null) {
final delta = jsonDecode(deltaString);
map['delta'] = delta;
// map.putIfAbsent(
// 'delta',
// () => delta,
// );
map[blockComponentDelta] = delta;
}
}
}
Attributes adapterCallback(Attributes map) => map
..putIfAbsent(
blockComponentDelta,
() => Delta().toJson(),
);
final adapter = {
ParagraphBlockKeys.type: (Attributes map) => map
..putIfAbsent(
'delta',
() => Delta().toJson(),
),
ParagraphBlockKeys.type: adapterCallback,
HeadingBlockKeys.type: adapterCallback,
CodeBlockKeys.type: adapterCallback,
QuoteBlockKeys.type: adapterCallback,
NumberedListBlockKeys.type: adapterCallback,
BulletedListBlockKeys.type: adapterCallback,
ToggleListBlockKeys.type: adapterCallback,
};
return adapter[ty]?.call(map) ?? map;
}
@@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
@@ -13,6 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Map<String, BlockComponentBuilder> getEditorBuilderMap({
@@ -93,8 +92,9 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
final factor = pageStyle.fontLayout.factor;
final headingPaddings = pageStyle.lineHeightLayout.headingPaddings
.map((e) => e * factor);
final level = node.attributes[HeadingBlockKeys.level] ?? 6;
return EdgeInsets.only(top: headingPaddings.elementAt(level));
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
level = level.clamp(1, 6);
return EdgeInsets.only(top: headingPaddings.elementAt(level - 1));
}
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
@@ -1,8 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
@@ -10,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/align_tool
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
@@ -29,6 +27,8 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
final codeBlockLocalization = CodeBlockLocalizations(
@@ -148,6 +148,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// code block
...codeBlockCharacterEvents,
// callout block
insertNewLineInCalloutBlock,
// toggle list
formatGreaterToToggleList,
insertChildNodeInsideToggleList,
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class EditorFontColors {
static final lightColors = [
const Color(0x00FFFFFF),
const Color(0xFFE8E0FF),
const Color(0xFFFFE6FD),
const Color(0xFFFFDAE6),
const Color(0xFFFFEFE3),
const Color(0xFFF5FFDC),
const Color(0xFFDDFFD6),
const Color(0xFFDEFFF1),
const Color(0xFFE1FBFF),
const Color(0xFFFFADAD),
const Color(0xFFFFE088),
const Color(0xFFA7DF4A),
const Color(0xFFD4C0FF),
const Color(0xFFFDB2FE),
const Color(0xFFFFD18B),
const Color(0xFF65E7F0),
const Color(0xFF71E6B4),
const Color(0xFF80F1FF),
];
static final darkColors = [
const Color(0x00FFFFFF),
const Color(0xFF8B80AD),
const Color(0xFF987195),
const Color(0xFF906D78),
const Color(0xFFA68B77),
const Color(0xFF88936D),
const Color(0xFF72936B),
const Color(0xFF6B9483),
const Color(0xFF658B90),
const Color(0xFF95405A),
const Color(0xFFA6784D),
const Color(0xFF6E9234),
const Color(0xFF6455A2),
const Color(0xFF924F83),
const Color(0xFFA48F34),
const Color(0xFF29A3AC),
const Color(0xFF2E9F84),
const Color(0xFF405EA6),
];
// if the input color doesn't exist in the list, return the input color itself.
static Color? fromBuiltInColors(BuildContext context, Color? color) {
if (color == null) {
return null;
}
final brightness = Theme.of(context).brightness;
// if the dark mode color using light mode, return it's corresponding light color. Same for light mode.
if (brightness == Brightness.light) {
if (darkColors.contains(color)) {
return lightColors[darkColors.indexOf(color)];
}
} else {
if (lightColors.contains(color)) {
return darkColors[lightColors.indexOf(color)];
}
}
return color;
}
}
@@ -56,7 +56,7 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node(
iconData: Icons.note,
keywords: [CalloutBlockKeys.type],
nodeBuilder: (editorState, context) =>
calloutNode(defaultColor: AFThemeExtension.of(context).calloutBGColor),
calloutNode(defaultColor: Colors.transparent),
replace: (_, node) => node.delta?.isEmpty ?? false,
updateSelection: (_, path, __, ___) {
return Selection.single(path: path, startOffset: 0);
@@ -0,0 +1,41 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
/// Pressing Enter in a callout block will insert a newline (\n) within the callout,
/// while pressing Shift+Enter in a callout will insert a new paragraph next to the callout.
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent insertNewLineInCalloutBlock =
CharacterShortcutEvent(
key: 'insert a new line in callout block',
character: '\n',
handler: _insertNewLineHandler,
);
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
final selection = editorState.selection?.normalized;
if (selection == null) {
return false;
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null || node.type != CalloutBlockKeys.type) {
return false;
}
// delete the selection
await editorState.deleteSelection(selection);
if (HardwareKeyboard.instance.isShiftPressed) {
await editorState.insertNewLine();
} else {
await editorState.insertTextAtCurrentSelection('\n');
}
return true;
};
@@ -149,22 +149,22 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
fontSize: 28.0,
fontWeight: FontWeight.w700,
fontFamily: fontFamily,
color: state.cover.type == PageStyleCoverImageType.none
? null
: Colors.white,
color:
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
),
onSubmitted: (value) {
scrollController.position.jumpTo(0);
context.read<ViewBloc>().add(ViewEvent.rename(value));
},
onChanged: _rename,
onSubmitted: _rename,
);
}
Widget _buildIcon(BuildContext context, String icon) {
return GestureDetector(
child: EmojiIconWidget(
emoji: icon,
emojiSize: 26,
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 34.0),
child: EmojiIconWidget(
emoji: icon,
emojiSize: 26,
),
),
onTap: () async {
final result = await context.push<EmojiPickerResult>(
@@ -247,4 +247,9 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
focusNode.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
}
}
void _rename(String name) {
scrollController.position.jumpTo(0);
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
}
@@ -74,17 +74,9 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
ClipboardServiceData(plainText: jsonEncode(node.toJson())),
);
},
text: SizedBox(
height: 52,
child: Row(
children: [
const HSpace(4),
FlowyText(
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
),
],
),
),
text: PlatformExtension.isDesktopOrWeb
? _buildDesktopErrorBlock(context)
: _buildMobileErrorBlock(context),
),
);
@@ -111,4 +103,44 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
return child;
}
Widget _buildDesktopErrorBlock(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const HSpace(4),
FlowyText.regular(
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
),
const HSpace(4),
FlowyText.regular(
'(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})',
color: Theme.of(context).hintColor,
),
],
),
);
}
Widget _buildMobileErrorBlock(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
),
const VSpace(6),
FlowyText.regular(
'(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})',
color: Theme.of(context).hintColor,
fontSize: 12.0,
),
],
),
);
}
}
@@ -1,13 +1,28 @@
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/util/levenshtein.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
final customizeFontToolbarItem = ToolbarItem(
id: 'editor.font',
@@ -16,10 +31,12 @@ final customizeFontToolbarItem = ToolbarItem(
builder: (context, editorState, highlightColor, _) {
final selection = editorState.selection!;
final popoverController = PopoverController();
final String? currentFontFamily = editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: FontFamilyDropDown(
currentFontFamily: '',
currentFontFamily: currentFontFamily ?? '',
offset: const Offset(0, 12),
popoverController: popoverController,
onOpen: () => keepEditorFocusNotifier.increase(),
@@ -35,8 +52,11 @@ final customizeFontToolbarItem = ToolbarItem(
Log.error('Failed to set font family: $e');
}
},
onResetFont: () async => editorState
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}),
onResetFont: () async {
popoverController.close();
await editorState
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FlowyTooltip(
@@ -52,3 +72,227 @@ final customizeFontToolbarItem = ToolbarItem(
);
},
);
class ThemeFontFamilySetting extends StatefulWidget {
const ThemeFontFamilySetting({
super.key,
required this.currentFontFamily,
});
final String currentFontFamily;
static Key textFieldKey = const Key('FontFamilyTextField');
static Key resetButtonkey = const Key('FontFamilyResetButton');
static Key popoverKey = const Key('FontFamilyPopover');
@override
State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();
}
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
@override
Widget build(BuildContext context) {
return SettingListTile(
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
onResetRequested: () {
context.read<AppearanceSettingsCubit>().resetFontFamily();
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
},
trailing: [
FontFamilyDropDown(currentFontFamily: widget.currentFontFamily),
],
);
}
}
class FontFamilyDropDown extends StatefulWidget {
const FontFamilyDropDown({
super.key,
required this.currentFontFamily,
this.onOpen,
this.onClose,
this.onFontFamilyChanged,
this.child,
this.popoverController,
this.offset,
this.showResetButton = false,
this.onResetFont,
});
final String currentFontFamily;
final VoidCallback? onOpen;
final VoidCallback? onClose;
final void Function(String fontFamily)? onFontFamilyChanged;
final Widget? child;
final PopoverController? popoverController;
final Offset? offset;
final bool showResetButton;
final VoidCallback? onResetFont;
@override
State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();
}
class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
final List<String> availableFonts = [
defaultFontFamily,
...GoogleFonts.asMap().keys,
];
final ValueNotifier<String> query = ValueNotifier('');
@override
void dispose() {
query.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final currentValue = widget.currentFontFamily.fontFamilyDisplayName;
return SettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: currentValue,
onClose: () {
query.value = '';
widget.onClose?.call();
},
offset: widget.offset,
child: widget.child,
popupBuilder: (_) {
widget.onOpen?.call();
return CustomScrollView(
shrinkWrap: true,
slivers: [
if (widget.showResetButton)
SliverPersistentHeader(
delegate: _ResetFontButton(onPressed: widget.onResetFont),
pinned: true,
),
SliverPadding(
padding: const EdgeInsets.only(right: 8),
sliver: SliverToBoxAdapter(
child: FlowyTextField(
key: ThemeFontFamilySetting.textFieldKey,
hintText:
LocaleKeys.settings_appearance_fontFamily_search.tr(),
autoFocus: false,
debounceDuration: const Duration(milliseconds: 300),
onChanged: (value) {
query.value = value;
},
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 4),
),
ValueListenableBuilder(
valueListenable: query,
builder: (context, value, child) {
var displayed = availableFonts;
if (value.isNotEmpty) {
displayed = availableFonts
.where(
(font) => font
.toLowerCase()
.contains(value.toLowerCase().toString()),
)
.sorted((a, b) => levenshtein(a, b))
.toList();
}
return SliverFixedExtentList.builder(
itemBuilder: (context, index) => _fontFamilyItemButton(
context,
getGoogleFontSafely(displayed[index]),
),
itemCount: displayed.length,
itemExtent: 32,
);
},
),
],
);
},
);
}
Widget _fontFamilyItemButton(
BuildContext context,
TextStyle style,
) {
final buttonFontFamily =
style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;
return Tooltip(
message: buttonFontFamily,
waitDuration: const Duration(milliseconds: 150),
child: SizedBox(
key: ValueKey(buttonFontFamily),
height: 32,
child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
buttonFontFamily.fontFamilyDisplayName,
fontFamily: buttonFontFamily,
),
rightIcon:
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(buttonFontFamily);
} else {
if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(buttonFontFamily);
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(buttonFontFamily);
}
}
PopoverContainer.of(context).close();
},
),
),
);
}
}
class _ResetFontButton extends SliverPersistentHeaderDelegate {
_ResetFontButton({this.onPressed});
final VoidCallback? onPressed;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Padding(
padding: const EdgeInsets.only(right: 8, bottom: 8.0),
child: FlowyTextButton(
LocaleKeys.document_toolbar_resetToDefaultFont.tr(),
fontColor: AFThemeExtension.of(context).textColor,
fontHoverColor: Theme.of(context).colorScheme.onSurface,
fontSize: 12,
onPressed: onPressed,
),
);
}
@override
double get maxExtent => 35;
@override
double get minExtent => 35;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
@@ -131,12 +131,16 @@ class _UnsplashImages extends StatelessWidget {
};
final mainAxisSpacing = switch (type) {
UnsplashImageType.halfScreen => 16.0,
UnsplashImageType.fullScreen => 8.0,
UnsplashImageType.fullScreen => 16.0,
};
final crossAxisSpacing = switch (type) {
UnsplashImageType.halfScreen => 10.0,
UnsplashImageType.fullScreen => 16.0,
};
return GridView.count(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: 10.0,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: 4 / 3,
children: photos
.map(
@@ -197,28 +201,31 @@ class _UnsplashImage extends StatelessWidget {
}
Widget _buildFullScreenImage(BuildContext context) {
return Stack(
children: [
LayoutBuilder(
builder: (context, constraints) {
return Image.network(
photo.urls.thumb.toString(),
fit: BoxFit.cover,
width: constraints.maxWidth,
height: constraints.maxHeight,
);
},
),
Positioned(
bottom: 6,
left: 6,
child: FlowyText.medium(
photo.name,
fontSize: 10.0,
color: Colors.white,
return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Stack(
children: [
LayoutBuilder(
builder: (context, constraints) {
return Image.network(
photo.urls.thumb.toString(),
fit: BoxFit.cover,
width: constraints.maxWidth,
height: constraints.maxHeight,
);
},
),
),
],
Positioned(
bottom: 9,
left: 10,
child: FlowyText.medium(
photo.name,
fontSize: 13.0,
color: Colors.white,
),
),
],
),
);
}
}
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
@@ -25,6 +23,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
@@ -110,40 +109,46 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
}
final iconSize = widget.textStyle?.fontSize ?? 16.0;
final child = GestureDetector(
onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 12,
textAlign: TextAlign.center,
lineHeight: 1.3,
)
: FlowySvg(
view.layout.icon,
size: Size.square(iconSize + 2.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: widget.textStyle?.fontSize,
fontWeight: widget.textStyle?.fontWeight,
),
const HSpace(2),
],
),
);
if (PlatformExtension.isMobile) {
return child;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 12,
textAlign: TextAlign.center,
lineHeight: 1.3,
)
: FlowySvg(
view.layout.icon,
size: Size.square(iconSize + 2.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: widget.textStyle?.fontSize,
fontWeight: widget.textStyle?.fontWeight,
),
const HSpace(2),
],
),
),
child: child,
),
);
},
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -100,14 +102,7 @@ class CustomMobileFloatingToolbar extends StatelessWidget {
Widget build(BuildContext context) {
return Animate(
autoPlay: true,
effects: [
const FadeEffect(duration: SelectionOverlay.fadeDuration),
MoveEffect(
curve: Curves.easeOutCubic,
begin: const Offset(0, 16),
duration: 100.milliseconds,
),
],
effects: _getEffects(context),
child: AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: buildMobileFloatingToolbarItems(
editorState,
@@ -120,4 +115,32 @@ class CustomMobileFloatingToolbar extends StatelessWidget {
),
);
}
List<Effect> _getEffects(BuildContext context) {
if (Platform.isIOS) {
final Size(:width, :height) = MediaQuery.of(context).size;
final alignmentX = (anchor.dx - width / 2) / (width / 2);
final alignmentY = (anchor.dy - height / 2) / (height / 2);
return [
ScaleEffect(
curve: Curves.easeInOut,
alignment: Alignment(alignmentX, alignmentY),
duration: 250.milliseconds,
),
];
} else if (Platform.isAndroid) {
return [
const FadeEffect(
duration: SelectionOverlay.fadeDuration,
),
MoveEffect(
curve: Curves.easeOutCubic,
begin: const Offset(0, 16),
duration: 100.milliseconds,
),
];
} else {
return [];
}
}
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
@@ -25,7 +26,10 @@ class ColorItem extends StatelessWidget {
editorState.getSelectionColor(AppFlowyRichTextKeys.textColor);
final String? selectedBackgroundColor =
editorState.getSelectionColor(AppFlowyRichTextKeys.backgroundColor);
final backgroundColor = EditorFontColors.fromBuiltInColors(
context,
selectedBackgroundColor?.tryToColor(),
);
return MobileToolbarMenuItemWrapper(
size: const Size(82, 52),
onTap: () async {
@@ -48,10 +52,12 @@ class ColorItem extends StatelessWidget {
);
},
icon: FlowySvgs.m_aa_font_color_m,
iconColor: selectedTextColor?.tryToColor(),
backgroundColor: selectedBackgroundColor?.tryToColor() ??
theme.toolbarMenuItemBackgroundColor,
selectedBackgroundColor: selectedBackgroundColor?.tryToColor(),
iconColor: EditorFontColors.fromBuiltInColors(
context,
selectedTextColor?.tryToColor(),
),
backgroundColor: backgroundColor ?? theme.toolbarMenuItemBackgroundColor,
selectedBackgroundColor: backgroundColor,
isSelected: selectedBackgroundColor != null,
showRightArrow: true,
iconPadding: const EdgeInsets.only(
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -154,7 +155,7 @@ class _TextColorAndBackgroundColorState
}
class _BackgroundColors extends StatelessWidget {
_BackgroundColors({
const _BackgroundColors({
this.selectedColor,
required this.onSelectedColor,
});
@@ -162,29 +163,11 @@ class _BackgroundColors extends StatelessWidget {
final Color? selectedColor;
final void Function(Color color) onSelectedColor;
final colors = [
const Color(0x00FFFFFF),
const Color(0xFFE8E0FF),
const Color(0xFFFFE6FD),
const Color(0xFFFFDAE6),
const Color(0xFFFFEFE3),
const Color(0xFFF5FFDC),
const Color(0xFFDDFFD6),
const Color(0xFFDEFFF1),
const Color(0xFFE1FBFF),
const Color(0xFFFFADAD),
const Color(0xFFFFE088),
const Color(0xFFA7DF4A),
const Color(0xFFD4C0FF),
const Color(0xFFFDB2FE),
const Color(0xFFFFD18B),
const Color(0xFFFFF176),
const Color(0xFF71E6B4),
const Color(0xFF80F1FF),
];
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).brightness == Brightness.light
? EditorFontColors.lightColors
: EditorFontColors.darkColors;
return GridView.count(
crossAxisCount: _count,
shrinkWrap: true,
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -122,11 +123,17 @@ final colorToolbarItem = AppFlowyMobileToolbarItem(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9),
color: backgroundColor?.tryToColor(),
color: EditorFontColors.fromBuiltInColors(
context,
backgroundColor?.tryToColor(),
),
),
child: FlowySvg(
FlowySvgs.m_aa_font_color_m,
color: textColor?.tryToColor(),
color: EditorFontColors.fromBuiltInColors(
context,
textColor?.tryToColor(),
),
),
);
},
@@ -1,16 +1,21 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/shared/permission/permission_checker.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
@@ -19,6 +24,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
class PageStyleCoverImage extends StatelessWidget {
@@ -33,13 +39,11 @@ class PageStyleCoverImage extends StatelessWidget {
final backgroundColor = context.pageStyleBackgroundColor;
return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Row(
return Column(
children: [
_buildOptionGroup(
context,
backgroundColor,
state,
),
_buildOptionGroup(context, backgroundColor, state),
const VSpace(16.0),
_buildPreview(context, state),
],
);
},
@@ -51,46 +55,124 @@ class PageStyleCoverImage extends StatelessWidget {
Color backgroundColor,
DocumentPageStyleState state,
) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(12),
right: Radius.circular(12),
return Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(12),
right: Radius.circular(12),
),
),
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
_CoverOptionButton(
showLeftCorner: true,
showRightCorner: false,
selected: state.coverImage.isPresets,
onTap: () => _showPresets(context),
child: const _PresetCover(),
),
),
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
_CoverOptionButton(
showLeftCorner: true,
showRightCorner: false,
selected: state.coverImage.isPresets,
onTap: () => _showPresets(context),
child: const _PresetCover(),
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: false,
selected: state.coverImage.isPhoto,
onTap: () => _pickImage(context),
child: const _PhotoCover(),
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: true,
selected: state.coverImage.isUnsplashImage,
onTap: () => _showUnsplash(context),
child: const _UnsplashCover(),
),
],
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: false,
selected: state.coverImage.isPhoto,
onTap: () => _pickImage(context),
child: const _PhotoCover(),
),
_CoverOptionButton(
showLeftCorner: false,
showRightCorner: true,
selected: state.coverImage.isUnsplashImage,
onTap: () => _showUnsplash(context),
child: const _UnsplashCover(),
),
],
),
);
}
Widget _buildPreview(
BuildContext context,
DocumentPageStyleState state,
) {
final cover = state.coverImage;
if (cover.isNone) {
return const SizedBox.shrink();
}
final value = cover.value;
final type = cover.type;
Widget preview = const SizedBox.shrink();
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
final userProfilePB =
context.read<MobileViewPageBloc>().state.userProfilePB;
preview = FlowyNetworkImage(
url: value,
userProfilePB: userProfilePB,
);
}
if (type == PageStyleCoverImageType.builtInImage) {
preview = Image.asset(
PageStyleCoverImageType.builtInImagePath(value),
fit: BoxFit.cover,
);
}
if (type == PageStyleCoverImageType.pureColor) {
final color = value.coverColor(context);
if (color != null) {
preview = ColoredBox(
color: color,
);
}
}
if (type == PageStyleCoverImageType.gradientColor) {
preview = Container(
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
preview = Image.file(
File(value),
fit: BoxFit.cover,
);
}
return Row(
children: [
FlowyText(LocaleKeys.pageStyle_image.tr()),
const Spacer(),
Container(
width: 40,
height: 28,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
border: Border.all(color: const Color(0x1F222533)),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
child: preview,
),
),
],
);
}
void _showPresets(BuildContext context) {
final pageStyleBloc = context.read<DocumentPageStyleBloc>();
context.pop();
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -99,18 +181,17 @@ class PageStyleCoverImage extends StatelessWidget {
showHeader: true,
showRemoveButton: true,
onRemove: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
pageStyleBloc.add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
},
title: LocaleKeys.pageStyle_pageCover.tr(),
barrierColor: Colors.transparent,
title: LocaleKeys.pageStyle_presets.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) {
return BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
value: pageStyleBloc,
child: const PageCoverBottomSheet(),
);
},
@@ -174,6 +255,9 @@ class PageStyleCoverImage extends StatelessWidget {
}
void _showUnsplash(BuildContext context) {
final pageStyleBloc = context.read<DocumentPageStyleBloc>();
context.pop();
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -181,15 +265,14 @@ class PageStyleCoverImage extends StatelessWidget {
showDoneButton: true,
showHeader: true,
showRemoveButton: true,
title: LocaleKeys.pageStyle_coverImage.tr(),
barrierColor: Colors.transparent,
title: LocaleKeys.pageStyle_unsplash.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
onRemove: () {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
pageStyleBloc.add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover.none(),
),
);
},
builder: (_) {
return ConstrainedBox(
@@ -204,14 +287,14 @@ class PageStyleCoverImage extends StatelessWidget {
child: UnsplashImageWidget(
type: UnsplashImageType.fullScreen,
onSelectUnsplashImage: (url) {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.unsplashImage,
value: url,
),
),
);
pageStyleBloc.add(
DocumentPageStyleEvent.updateCoverImage(
PageStyleCover(
type: PageStyleCoverImageType.unsplashImage,
value: url,
),
),
);
},
),
),
@@ -308,6 +391,7 @@ class _CoverOptionButton extends StatelessWidget {
duration: Durations.medium1,
decoration: selected
? ShapeDecoration(
color: const Color(0x141AC3F2),
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
@@ -11,8 +11,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:go_router/go_router.dart';
class PageStyleIcon extends StatelessWidget {
class PageStyleIcon extends StatefulWidget {
const PageStyleIcon({
super.key,
required this.view,
@@ -20,10 +21,15 @@ class PageStyleIcon extends StatelessWidget {
final ViewPB view;
@override
State<PageStyleIcon> createState() => _PageStyleIconState();
}
class _PageStyleIconState extends State<PageStyleIcon> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PageStyleIconBloc(view: view)
create: (_) => PageStyleIconBloc(view: widget.view)
..add(const PageStyleIconEvent.initial()),
child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(
builder: (context, state) {
@@ -60,6 +66,10 @@ class PageStyleIcon extends StatelessWidget {
}
void _showIconSelector(BuildContext context, String selectedIcon) {
context.pop();
final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
..add(const PageStyleIconEvent.initial());
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -67,7 +77,6 @@ class PageStyleIcon extends StatelessWidget {
showDoneButton: true,
showHeader: true,
title: LocaleKeys.titleBar_pageIcon.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
isScrollControlled: true,
enableDraggableScrollable: true,
@@ -75,13 +84,13 @@ class PageStyleIcon extends StatelessWidget {
initialChildSize: 0.61,
showRemoveButton: true,
onRemove: () {
context.read<PageStyleIconBloc>().add(
const PageStyleIconEvent.updateIcon('', true),
);
pageStyleIconBloc.add(
const PageStyleIconEvent.updateIcon('', true),
);
},
scrollableWidgetBuilder: (_, controller) {
return BlocProvider.value(
value: context.read<PageStyleIconBloc>(),
value: pageStyleIconBloc,
child: Expanded(
child: Scrollbar(
controller: controller,
@@ -112,6 +121,8 @@ class _IconSelectorState extends State<_IconSelector> {
EmojiData? emojiData;
List<String> availableEmojis = [];
PageStyleIconBloc? pageStyleIconBloc;
@override
void initState() {
super.initState();
@@ -131,6 +142,14 @@ class _IconSelectorState extends State<_IconSelector> {
},
);
}
pageStyleIconBloc = context.read<PageStyleIconBloc>();
}
@override
void dispose() {
pageStyleIconBloc?.close();
super.dispose();
}
@override
@@ -146,7 +165,7 @@ class _IconSelectorState extends State<_IconSelector> {
_buildSearchBar(context),
Expanded(
child: GridView.count(
crossAxisCount: _getEmojiPerLine(context),
crossAxisCount: 7,
controller: widget.scrollController,
children: [
for (final emoji in availableEmojis)
@@ -165,27 +184,33 @@ class _IconSelectorState extends State<_IconSelector> {
String emoji,
String? selectedEmoji,
) {
Widget child = Center(
child: FlowyText.emoji(
emoji,
fontSize: 24,
Widget child = SizedBox.square(
dimension: 24.0,
child: Center(
child: FlowyText.emoji(
emoji,
fontSize: 24,
),
),
);
if (emoji == selectedEmoji) {
child = Container(
margin: const EdgeInsets.all(8.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
child = Center(
child: Container(
width: 40,
height: 40,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.40,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(10),
),
borderRadius: BorderRadius.circular(9),
),
child: child,
),
child: child,
);
}
@@ -208,11 +233,6 @@ class _IconSelectorState extends State<_IconSelector> {
return availableEmojis;
}
int _getEmojiPerLine(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width ~/ 48.0; // the size of the emoji
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
@@ -11,6 +11,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
const kPageStyleLayoutHeight = 52.0;
@@ -117,6 +118,7 @@ class _OptionGroup<T> extends StatelessWidget {
duration: Durations.medium1,
decoration: selected
? ShapeDecoration(
color: const Color(0x141AC3F2),
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
@@ -180,7 +182,10 @@ class _FontButton extends StatelessWidget {
const HSpace(16.0),
FlowyText(LocaleKeys.titleBar_font.tr()),
const Spacer(),
FlowyText(fontFamilyDisplayName),
FlowyText(
fontFamilyDisplayName,
color: context.pageStyleTextColor,
),
const HSpace(6.0),
const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),
const HSpace(12.0),
@@ -193,6 +198,9 @@ class _FontButton extends StatelessWidget {
}
void _showFontSelector(BuildContext context) {
final pageStyleBloc = context.read<DocumentPageStyleBloc>();
context.pop();
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -200,7 +208,6 @@ class _FontButton extends StatelessWidget {
showDoneButton: true,
showHeader: true,
title: LocaleKeys.titleBar_font.tr(),
barrierColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
isScrollControlled: true,
enableDraggableScrollable: true,
@@ -208,7 +215,7 @@ class _FontButton extends StatelessWidget {
initialChildSize: 0.61,
scrollableWidgetBuilder: (_, controller) {
return BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
value: pageStyleBloc,
child: BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
builder: (context, state) {
return Expanded(
@@ -219,11 +226,11 @@ class _FontButton extends StatelessWidget {
selectedFontFamilyName:
state.fontFamily ?? defaultFontFamily,
onFontFamilySelected: (fontFamilyName) {
context.read<DocumentPageStyleBloc>().add(
DocumentPageStyleEvent.updateFontFamily(
fontFamilyName,
),
);
pageStyleBloc.add(
DocumentPageStyleEvent.updateFontFamily(
fontFamilyName,
),
);
},
),
),
@@ -25,7 +25,7 @@ class PageStyleBottomSheet extends StatelessWidget {
children: [
// cover image
FlowyText(
LocaleKeys.pageStyle_backgroundImage.tr(),
LocaleKeys.pageStyle_coverImage.tr(),
color: context.pageStyleTextColor,
fontSize: 14.0,
),
@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
@@ -41,16 +42,20 @@ class EditorStyleCustomizer {
EditorStyle desktop() {
final theme = Theme.of(context);
final appearanceFont = context.read<AppearanceSettingsCubit>().state.font;
final appearance = context.read<DocumentAppearanceCubit>().state;
final fontSize = appearance.fontSize;
final fontFamily = appearance.fontFamily;
String fontFamily = appearance.fontFamily;
if (fontFamily.isEmpty && appearanceFont.isNotEmpty) {
fontFamily = appearanceFont;
}
return EditorStyle.desktop(
padding: padding,
cursorColor: appearance.cursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
DefaultAppearanceSettings.getDefaultCursorColor(context),
selectionColor: appearance.selectionColor ??
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
DefaultAppearanceSettings.getDefaultSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
text: baseTextStyle(fontFamily).copyWith(
@@ -95,6 +100,8 @@ class EditorStyleCustomizer {
final fontFamily = pageStyle.fontFamily ?? defaultFontFamily;
final defaultTextDirection =
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
final textScaleFactor =
context.read<AppearanceSettingsCubit>().state.textScaleFactor;
final baseTextStyle = this.baseTextStyle(fontFamily);
final codeFontSize = max(0.0, fontSize - 2);
return EditorStyle.mobile(
@@ -131,8 +138,7 @@ class EditorStyleCustomizer {
textSpanDecorator: customizeAttributeDecorator,
mobileDragHandleBallSize: const Size.square(12.0),
magnifierSize: const Size(144, 96),
textScaleFactor:
context.watch<AppearanceSettingsCubit>().state.textScaleFactor,
textScaleFactor: textScaleFactor,
);
}
@@ -213,7 +219,7 @@ class EditorStyleCustomizer {
);
TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
if (fontFamily == null) {
if (fontFamily == null || fontFamily == defaultFontFamily) {
return TextStyle(fontWeight: fontWeight);
}
try {
@@ -241,6 +247,21 @@ class EditorStyleCustomizer {
return before;
}
if (attributes.backgroundColor != null) {
final color = EditorFontColors.fromBuiltInColors(
context,
attributes.backgroundColor!,
);
if (color != null) {
return TextSpan(
text: before.text,
style: after.style?.merge(
TextStyle(backgroundColor: color),
),
);
}
}
// try to refresh font here.
if (attributes.fontFamily != null) {
try {
@@ -0,0 +1,87 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class ConditionalListenableBuilder<T> extends StatefulWidget {
const ConditionalListenableBuilder({
super.key,
required this.valueListenable,
required this.buildWhen,
required this.builder,
this.child,
});
/// The [ValueListenable] whose value you depend on in order to build.
///
/// This widget does not ensure that the [ValueListenable]'s value is not
/// null, therefore your [builder] may need to handle null values.
final ValueListenable<T> valueListenable;
/// The [buildWhen] function will be called on each value change of the
/// [valueListenable]. If the [buildWhen] function returns true, the [builder]
/// will be called with the new value of the [valueListenable].
///
final bool Function(T previous, T current) buildWhen;
/// A [ValueWidgetBuilder] which builds a widget depending on the
/// [valueListenable]'s value.
///
/// Can incorporate a [valueListenable] value-independent widget subtree
/// from the [child] parameter into the returned widget tree.
final ValueWidgetBuilder<T> builder;
/// A [valueListenable]-independent widget which is passed back to the [builder].
///
/// This argument is optional and can be null if the entire widget subtree the
/// [builder] builds depends on the value of the [valueListenable]. For
/// example, in the case where the [valueListenable] is a [String] and the
/// [builder] returns a [Text] widget with the current [String] value, there
/// would be no useful [child].
final Widget? child;
@override
State<ConditionalListenableBuilder> createState() =>
_ConditionalListenableBuilderState<T>();
}
class _ConditionalListenableBuilderState<T>
extends State<ConditionalListenableBuilder<T>> {
late T value;
@override
void initState() {
super.initState();
value = widget.valueListenable.value;
widget.valueListenable.addListener(_valueChanged);
}
@override
void didUpdateWidget(ConditionalListenableBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.valueListenable != widget.valueListenable) {
oldWidget.valueListenable.removeListener(_valueChanged);
value = widget.valueListenable.value;
widget.valueListenable.addListener(_valueChanged);
}
}
@override
void dispose() {
widget.valueListenable.removeListener(_valueChanged);
super.dispose();
}
void _valueChanged() {
if (widget.buildWhen(value, widget.valueListenable.value)) {
setState(() {
value = widget.valueListenable.value;
});
} else {
value = widget.valueListenable.value;
}
}
@override
Widget build(BuildContext context) {
return widget.builder(context, value, widget.child);
}
}
@@ -1,7 +1,14 @@
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
const _defaultFontFamilies = [
defaultFontFamily,
builtInCodeFontFamily,
fallbackFontFamily,
];
// if the font family is not available, google fonts packages will throw an exception
// this method will return the system font family if the font family is not available
TextStyle getGoogleFontSafely(
@@ -12,19 +19,31 @@ TextStyle getGoogleFontSafely(
double? letterSpacing,
double? lineHeight,
}) {
try {
return GoogleFonts.getFont(
fontFamily,
// if the font family is the built-in font family, we can use it directly
if (_defaultFontFamilies.contains(fontFamily)) {
return TextStyle(
fontFamily: fontFamily.isEmpty ? null : fontFamily,
fontWeight: fontWeight,
fontSize: fontSize,
color: fontColor,
letterSpacing: letterSpacing,
height: lineHeight,
);
} catch (e) {
Log.error(
'Font family $fontFamily is not available, using default font family instead',
);
} else {
try {
return GoogleFonts.getFont(
fontFamily,
fontWeight: fontWeight,
fontSize: fontSize,
color: fontColor,
letterSpacing: letterSpacing,
height: lineHeight,
);
} catch (e) {
Log.error(
'Font family $fontFamily is not available, using default font family instead',
);
}
}
return TextStyle(
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:window_manager/window_manager.dart';
class WindowsButtonListener extends WindowListener {
WindowsButtonListener();
final ValueNotifier<bool> isMaximized = ValueNotifier(false);
@override
void onWindowMaximize() => isMaximized.value = true;
@override
void onWindowUnmaximize() => isMaximized.value = false;
void dispose() => isMaximized.dispose();
}
class WindowTitleBar extends StatefulWidget {
const WindowTitleBar({
super.key,
this.leftChildren = const [],
});
final List<Widget> leftChildren;
@override
State<WindowTitleBar> createState() => _WindowTitleBarState();
}
class _WindowTitleBarState extends State<WindowTitleBar> {
late final WindowsButtonListener? windowsButtonListener;
bool isMaximized = false;
@override
void initState() {
super.initState();
if (PlatformExtension.isWindows || PlatformExtension.isLinux) {
windowsButtonListener = WindowsButtonListener();
windowManager.addListener(windowsButtonListener!);
windowsButtonListener!.isMaximized.addListener(() {
if (mounted) {
setState(
() => isMaximized = windowsButtonListener!.isMaximized.value,
);
}
});
} else {
windowsButtonListener = null;
}
windowManager.isMaximized().then(
(v) => mounted ? setState(() => isMaximized = v) : null,
);
}
@override
void dispose() {
if (windowsButtonListener != null) {
windowManager.removeListener(windowsButtonListener!);
windowsButtonListener?.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final brightness = Theme.of(context).brightness;
return Container(
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
child: DragToMoveArea(
child: Row(
children: [
const HSpace(4),
...widget.leftChildren,
const Spacer(),
WindowCaptionButton.minimize(
brightness: brightness,
onPressed: () => windowManager.minimize(),
),
if (isMaximized) ...[
WindowCaptionButton.unmaximize(
brightness: brightness,
onPressed: () => windowManager.unmaximize(),
),
] else ...[
WindowCaptionButton.maximize(
brightness: brightness,
onPressed: () => windowManager.maximize(),
),
],
WindowCaptionButton.close(
brightness: brightness,
onPressed: () => windowManager.close(),
),
],
),
),
);
}
}
@@ -6,8 +6,6 @@ import 'package:flutter/material.dart';
class AppFlowyApplication implements EntryPoint {
@override
Widget create(LaunchConfiguration config) {
return SplashScreen(
isAnon: config.isAnon,
);
return SplashScreen(isAnon: config.isAnon);
}
}
Loaded 100 of 501 files, more files were not shown because too many files have changed in this diff. Show more