Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2978e0d6c | ||
|
|
b7bc847107 | ||
|
|
aa07393253 | ||
|
|
e85dbe724c | ||
|
|
24b3d69860 | ||
|
|
c67e266174 | ||
|
|
d303d5affd | ||
|
|
e6d166df3b | ||
|
|
9b7ee4b978 | ||
|
|
0c0bd54f52 | ||
|
|
f1b035e362 | ||
|
|
bd62a161be | ||
|
|
7e83d598f8 | ||
|
|
bfc6cd721e | ||
|
|
3c6ce4ea4d | ||
|
|
c1e7e72154 | ||
|
|
759998cacb | ||
|
|
a6467c1b16 | ||
|
|
edfd1ab116 | ||
|
|
c3c99c7960 | ||
|
|
3bead0bab9 | ||
|
|
eb0c9d3264 | ||
|
|
5c7ba0466f | ||
|
|
b1177ad3ea | ||
|
|
b9faf3b24a | ||
|
|
12ff548b09 | ||
|
|
027ab2c1ef | ||
|
|
8273d66c50 | ||
|
|
38fa9f7942 | ||
|
|
39f8c47758 | ||
|
|
4cfd83cbc4 | ||
|
|
cdcb393efd | ||
|
|
a0ed043cb8 | ||
|
|
f47c88b022 | ||
|
|
c732438f8a | ||
|
|
f3b8ec8a04 | ||
|
|
a490f34a61 | ||
|
|
28a27d1b67 | ||
|
|
6edb184bfb | ||
|
|
dbbdc13d96 | ||
|
|
eeddf341e1 | ||
|
|
aaa72fc143 |
No files matched your search
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
+4
-1
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
+32
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
-2
@@ -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();
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
|
||||
-93
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+18
-14
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+139
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+231
@@ -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;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+67
@@ -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';
|
||||
+140
-71
@@ -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,
|
||||
+19
-10
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+99
-104
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
}
|
||||
|
||||
deleteRow
|
||||
? RowBackendService.deleteRow(viewId, rowId)
|
||||
? RowBackendService.deleteRows(viewId, [rowId])
|
||||
: RowBackendService.duplicateRow(viewId, rowId);
|
||||
|
||||
context
|
||||
|
||||
+4
-4
@@ -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),
|
||||
|
||||
+9
-1
@@ -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(
|
||||
|
||||
+1
-5
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
);
|
||||
|
||||
+4
-5
@@ -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(
|
||||
|
||||
+93
@@ -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,
|
||||
}
|
||||
+260
-224
@@ -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;
|
||||
}
|
||||
+547
-150
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
+74
-64
@@ -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(
|
||||
|
||||
+365
@@ -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];
|
||||
}
|
||||
}
|
||||
+159
-140
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+146
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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),
|
||||
|
||||
+2
-2
@@ -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));
|
||||
},
|
||||
|
||||
+1
-1
@@ -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];
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+12
-3
@@ -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,
|
||||
|
||||
-56
@@ -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
|
||||
|
||||
+98
-86
@@ -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: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -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(
|
||||
|
||||
+3
-1
@@ -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) {
|
||||
|
||||
+4
-2
@@ -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(
|
||||
|
||||
+22
-32
@@ -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) {
|
||||
|
||||
+3
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-11
@@ -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;
|
||||
}
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+41
@@ -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;
|
||||
};
|
||||
+15
-10
@@ -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));
|
||||
}
|
||||
}
|
||||
+43
-11
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+248
-4
@@ -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;
|
||||
}
|
||||
+30
-23
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-32
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+31
-8
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-5
@@ -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(
|
||||
|
||||
+5
-22
@@ -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,
|
||||
|
||||
+9
-2
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+147
-63
@@ -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,
|
||||
|
||||
+47
-27
@@ -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(
|
||||
|
||||
+15
-8
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -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
Reference in new issue
Block a user