Compare commits

...
41 Commits
Author SHA1 Message Date
Lucas.Xu d36a846425 chore: update CHANGELOG.md 2023-09-26 16:55:49 +08:00
Lucas.Xu a02ec98e9c fix: undo failed in nested list 2023-09-26 16:55:49 +08:00
Lucas.Xu 32c06cf1bf fix: clear selection when editing inline database view 2023-09-26 16:55:49 +08:00
Lucas.Xu cf1096b516 fix: undo failed in nested list 2023-09-26 16:55:49 +08:00
Lucas.Xu 938953d7b3 fix: unable to parse html text which contains <mark> tag (#3529) 2023-09-26 14:37:10 +08:00
Mathias Mogensen 949dab9afe fix: on hover/enter issue for view items (#3528) 2023-09-25 12:12:43 +02:00
Narayan 0996f5fd24 fix: missing the hand gesture icon in the text alignment options (#3509) 2023-09-24 21:42:11 +08:00
Lucas.Xu 6e887f1f12 chore: bump version 0.3.3 (#3517) 2023-09-24 20:28:31 +08:00
Richard Shiue ab9338eb89 fix: some issues with the end date (#3495) 2023-09-24 20:26:18 +08:00
Lakhan Baheti fd4088aed0 fix: grid theming to bottom row count number (#3510)
* fix: theming to bottom count number

* refactor: Row to RichText
2023-09-24 20:25:47 +08:00
Richard Shiue 375ff5ed6d refactor: use same bloc in grid and cards (#3511) 2023-09-24 20:24:38 +08:00
Ahsan Ahmad 847290d9a4 fix: remove max length of 30 while creating a new select option (#3513) 2023-09-24 19:14:25 +08:00
Nathan.fooo 56687581f2 feat: using script to update the collab source. (#3508)
* chore: add script

* chore: update script

* chore: update bytes version

* chore: submit lock file
2023-09-24 15:50:58 +08:00
Jannes Blobel 3be39f5e7b chore: update inlang project settings (#3441) 2023-09-23 23:41:56 +08:00
Lucas.Xu 1997231adf Merge pull request #3500 from LucasXu0/fix_032_launch_review_issues
fix launch review(v0.3.3) issues
2023-09-23 12:50:57 +08:00
Lucas.Xu 89face4f02 test: add integration test for ltr/rtl mode 2023-09-23 11:31:08 +08:00
Richard Shiue 37ddce3a29 chore: calendar UI polish (#3484)
* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests
2023-09-23 09:47:48 +08:00
Nitin-Poojary 593df96b10 fix: made cursor grab and background color when hovering on Appearance Options Buttons (#3498) 2023-09-22 19:03:18 +02:00
Mathias Mogensen 1b966171c4 feat: enable removing user icon (#3487)
* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes
2023-09-22 15:20:07 +02:00
Lakhan Baheti 047f1a0b39 fix: right icon size sam as left one (#3494) 2023-09-22 12:25:27 +02:00
Lucas.Xu f2f3506b29 fix: unable to change RTL of heading block 2023-09-22 17:13:08 +08:00
Lucas.Xu 0ccced4b54 chore: rename package name (#3501) 2023-09-22 16:32:18 +08:00
Lucas.Xu f762a564b5 feat: only show text direction toolbar item when RTL is enabled 2023-09-22 15:59:21 +08:00
Lucas.Xu 57b78ee3c6 feat: add reset font button in toolbar 2023-09-22 15:49:11 +08:00
Lucas.Xu 5dfc470873 chore: add more space between font item and font menu 2023-09-22 13:53:14 +08:00
Aryan More d5f8ce5435 fix: add padding below field title text field (#3440)
* Fixed Issue no #3426

* Reversed the pubspec.lock mistaken update

* FIXED PADDING

* Fixed Padding issue on calender field edit popup
2023-09-22 11:15:18 +08:00
Richard Shiue 3c65a96b04 chore: checklist ux flow redesign (#3418)
* chore: ux flow redesign

* chore: remove unused imports

* fix: allow creation of tasks of the same name

* chore: apply code suggestions from Mathias
2023-09-22 09:33:24 +08:00
Nathan.fooo 6ba7fc0317 feat: openai and stabilityai integration (#3439)
* chore: create trait

* test: add tests

* chore: remove log

* chore: disable log
2023-09-21 12:41:52 +08:00
Vincenzo De Petris 37a5054ebd fix: close popover after item selection in settings view (#3362)
* fix: close popover after item selection in settings view

* fix: add missing await before closing popover

* fix: find popover container by context instead of passing controllers around

* fix: add requested changes

* feat: close text direction settings popups after selection

* fix: clean up

* fix: restore theme value dropdown as StatefulWidget
2023-09-21 11:23:28 +08:00
Mathias Mogensen 048434024b feat: select cover image on upload (#3488) 2023-09-21 04:02:39 +02:00
Nitin-Poojary 2c757e9b6c fix: set minimum width for grid fields to 50px (#3397)
* fix: set minimum width for grid field to 100px

* test: add grid field width test

* fix: field width should not be less then 50px

* test: grid field width should not be less then 50px

* fix: cursor-based resizing issue

* test: updated tests
2023-09-21 09:39:13 +08:00
Lucas.Xu 9c59e1487e feat: customize text font (#3467)
* feat: update UI in settings page

* feat: customzing font in document page

* fix: flutter analyze and format issues
2023-09-21 09:12:25 +08:00
Mathias Mogensen 4b9b723521 fix: add bottom border to new property column (#3486) 2023-09-21 03:10:41 +02:00
Vyshnav Ajith a389836897 Added Language Support for Urdu. Issue#3372 (#3469)
* Create ur.json

* Add files via upload

* changes

* Added Locale change to the supported Locales List

* Added Urdu in Language.dart

* Delete .vscode/launch.json

* Changed the icon for Calendar
2023-09-20 17:31:17 +02:00
Mathias Mogensen eec7723ce0 fix: tests for interacting with viewitem (#3472) 2023-09-20 23:14:04 +08:00
Lucas.Xu 2f938a331f Merge pull request #3468 from LucasXu0/fix_build_error_on_xcode15
fix: build error on Xcode 15
2023-09-20 20:32:28 +08:00
Lucas.Xu 847d7cb35a fix: build error on Xcode 15 2023-09-20 15:28:27 +08:00
Onyedika Israel Ukwueze 094f8fc307 fix: visual bug on board accessory 2023-09-19 16:02:19 +08:00
Yatendra Kumarandnathan e3f11ea9c0 feat: integrated openAI rust plugin (#3302)
* feat: create flowy-plugins

* feat: integrated openai service with interface

* feat: integrated openai api call

* fix: response in api

* fix: removed unused imports

* fix: remove parse.rs

* fix: macos version + removed print

* fix: removed debugPrint + changed deployment target

* fix: android project+removed gesture detector

* fix: removed unused imports

* chore: fix compile

* chore: rever changes

* chore: rever changes

* chore: fix clippy warnings

* chore: rename

* chore: fix compile error

* chore: remove dart ai

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-19 10:37:37 +08:00
Richard Shiueandnathan 124d435f09 feat: add end date to time cell data (#3369)
* feat: add end date to time cell data

* feat: implement ui for end time

* test: add date to text test

* chore: clippy warnings

* fix: unexpected time parsing

* fix: fix the date logic when toggling end date

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-19 09:58:15 +08:00
Mathias Mogensen b700f95c7f fix: do not rebuild hover on item selection (#3423) 2023-09-18 16:39:30 +08:00
186 changed files with 4711 additions and 2013 deletions

No files matched your search

+38 -14
View File
@@ -35,13 +35,15 @@ jobs:
release_name: v${{ github.ref }}
body_path: ${{ env.RELEASE_NOTES_PATH }}
# the package name should be with the format: AppFlowy-<version>-<os>-<arch>
build-for-windows:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: create-release
env:
WINDOWS_APP_RELEASE_PATH: frontend\appflowy_flutter\product\${{ github.ref_name }}\windows
WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip
WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
WINDOWS_ZIP_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64.zip
WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
@@ -133,8 +135,8 @@ jobs:
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64
strategy:
fail-fast: false
matrix:
@@ -235,8 +237,8 @@ jobs:
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_AARCH64_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-universal.zip
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-universal
MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal
strategy:
fail-fast: false
matrix:
@@ -339,9 +341,11 @@ jobs:
needs: create-release
env:
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release
LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
# PKG_CONFIG_SYSROOT_DIR: /
LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
strategy:
fail-fast: false
matrix:
@@ -385,6 +389,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get -y install alien
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli
@@ -409,8 +414,17 @@ jobs:
- name: Build Linux package (.deb)
working-directory: frontend
run: |
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_NAME }}
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }}
- name: Build Linux package (.rpm)
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
run: |
ls -a
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
echo '------------'
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
ls -a
- name: Upload Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
@@ -423,15 +437,25 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Debian package
id: upload-release-asset-install-package
id: upload-release-asset-install-package-deb
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_DEB_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_DEB_NAME }}
asset_content_type: application/octet-stream
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
- name: Upload RPM package
id: upload-release-asset-install-package-rpm
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_RPM_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}
asset_content_type: application/octet-stream
build-for-docker:
+18
View File
@@ -1,5 +1,23 @@
# Release Notes
## Version 0.3.3 - 09/24/2023
### New Features
- Added an end date field to the time cell in the database.
- Added Support for customizing the font family from GoogleFonts in the editor.
- Set the uploaded image to cover by default.
- Added Support for resetting the user icon on settings page
- Add Urdu language translations.
### Bug fixes
- Default colors for the blocks except for the callout were not transparent.
- Option/Alt + click to add a block above didn't work on the first line.
- Unable to paste HTML content containing `<mark>` tag.
- Unable to select the text from anywhere in the line.
- The selection in the editor didn't clear when editing the inline database.
- Added a bottom border to new property column in the database.
- Set minimum width of 50px for grid fields.
## Version 0.3.2 - 09/18/2023
### New Features
+1 -1
View File
@@ -25,7 +25,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.3.2"
CURRENT_APP_VERSION = "0.3.3"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -80,7 +80,8 @@ void main() {
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
expect(
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
findsOneWidget,
);
});
@@ -79,33 +79,33 @@ void main() {
// Tap on create new event button
await tester.tapAddCalendarEventButton();
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
tester.assertNumberOfEventsInCalendar(1);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(2);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertRowDetailPageOpened();
tester.assertEventEditorOpen();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
await tester.dismissRowDetailPage();
await tester.editEventTitle('hello world');
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
@@ -113,6 +113,10 @@ void main() {
// Click on the event
await tester.openCalendarEvent(index: 1);
tester.assertEventEditorOpen();
// Click on the open icon
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
// Duplicate the event
@@ -126,12 +130,23 @@ void main() {
// Delete an event
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Delete event from row detail page
await tester.openCalendarEvent(index: 1);
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 0 event
tester.assertNumberOfEventsInCalendar(0, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
});
testWidgets('rescheduling events', (tester) async {
@@ -145,7 +160,7 @@ void main() {
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
@@ -157,13 +172,12 @@ void main() {
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Make sure that the event is today
tester.assertNumberOfEventsOnSpecificDay(1, today);
@@ -182,12 +196,43 @@ void main() {
await tester.selectDay(content: newDate.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Dismiss the event editor
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that nothing shows up
tester.findUnscheduledPopup(findsNothing, 0);
// Click on the event in the calendar
await tester.openCalendarEvent(index: 0, date: newDate);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Clear the date of the event
await tester.clearDate();
// Dismiss the event editor
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(0);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that a popup appears and 1 unscheduled event
tester.findUnscheduledPopup(findsOneWidget, 1);
// Click on the unscheduled event
await tester.clickUnscheduledEvent();
tester.assertRowDetailPageOpened();
});
});
}
@@ -185,7 +185,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit time cell', (tester) async {
testWidgets('edit date time cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -456,9 +456,6 @@ void main() {
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// assert that new task editor is shown
tester.assertNewCheckListTaskEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
@@ -481,7 +478,6 @@ void main() {
// dismiss new task editor
await tester.dismissCellEditor();
tester.assertNewCheckListTaskEditorVisible(visible: false);
// dismiss checklist cell editor
await tester.dismissCellEditor();
@@ -489,11 +485,8 @@ void main() {
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// start editing the first checklist cell again, click on new task button
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
tester.assertNewCheckListTaskEditorVisible(visible: false);
await tester.tapChecklistNewTaskButton();
tester.assertNewCheckListTaskEditorVisible(visible: true);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
@@ -540,9 +533,6 @@ void main() {
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// assert that the new task editor is shown
tester.assertNewCheckListTaskEditorVisible(visible: true);
// dismiss the cell editor
await tester.dismissCellEditor();
@@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// +, ... button beside the block component.
group('document with option action button', () {
testWidgets(
'click + to add a block after current selection, and click + and option key to add a block before current selection',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
var editorState = tester.editor.getCurrentEditorState();
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty);
// add a new block after the current selection
await tester.editor.hoverAndClickOptionAddButton([0], false);
// await tester.pumpAndSettle();
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty);
// cancel the selection menu
await tester.tapAt(Offset.zero);
await tester.editor.hoverAndClickOptionAddButton([0], true);
await tester.pumpAndSettle();
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
// cancel the selection menu
await tester.tapAt(Offset.zero);
await tester.tapAt(Offset.zero);
await tester.createNewPageWithName(name: 'test');
await tester.openPage(gettingStarted);
// check the status again
editorState = tester.editor.getCurrentEditorState();
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
});
});
}
@@ -5,6 +5,8 @@ import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
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_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;
import 'document_with_inline_math_equation_test.dart'
@@ -29,4 +31,6 @@ void startTesting() {
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
document_option_action_test.main();
}
@@ -0,0 +1,58 @@
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('text direction', () {
testWidgets(
'''no text direction items will be displayed in the default/LTR mode,and three text direction items will be displayed in the RTL mode.''',
(tester) async {
// combine the two tests into one to avoid the time-consuming process of initializing the app
await tester.initializeAppFlowy();
await tester.tapGoButton();
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 1,
);
// click the first line of the readme
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
await tester.pumpAndSettle();
// because this icons are defined in the appflowy_editor package, we can't fetch the icons by SVG data. [textDirectionItems]
final textDirectionIconNames = [
'toolbar/text_direction_auto',
'toolbar/text_direction_left',
'toolbar/text_direction_right',
];
// no text direction items in default/LTR mode
var button = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
textDirectionIconNames.contains(widget.iconName),
);
expect(button, findsNothing);
// switch to the RTL mode
await tester.switchLayoutDirectionMode(LayoutDirection.rtlLayout);
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
await tester.pumpAndSettle();
button = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
textDirectionIconNames.contains(widget.iconName),
);
expect(button, findsNWidgets(3));
});
});
}
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -31,6 +32,18 @@ void main() {
),
findsOneWidget,
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
// test: the selection of editor should be clear when editing the grid
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [1]),
),
);
final gridTextCell = find.byType(GridTextCell).first;
await tester.tapButton(gridTextCell);
expect(tester.editor.getCurrentEditorState().selection, isNull);
});
testWidgets('insert a referenced board', (tester) async {
@@ -0,0 +1,54 @@
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings: user icon tests', () {
testWidgets('select icon, select default option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userAvatarFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserAvatar),
);
// Open icon picker dialog
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(IconOption).first);
await tester.pumpAndSettle();
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isNotEmpty);
// Open icon picker dialog again
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Tap the default option
await tester.tap(
find.descendant(
of: find.byType(IconGallery),
matching: find.byType(UserAvatar),
),
);
await tester.pumpAndSettle();
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isEmpty);
});
});
}
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
@@ -25,6 +26,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
@@ -45,7 +47,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
@@ -330,7 +331,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> toggleIncludeTime() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateEditor = find.byType(IncludeTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
matching: findToggle,
);
await tapButton(finder);
}
Future<void> toggleDateRange() async {
final findDateEditor = find.byType(EndTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
@@ -483,15 +494,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
void assertNewCheckListTaskEditorVisible({required bool visible}) {
final editor = find.byType(NewTaskItem);
if (visible) {
expect(editor, findsOneWidget);
} else {
expect(editor, findsNothing);
}
}
Future<void> createNewChecklistTask({
required String name,
enter = false,
@@ -504,10 +506,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await enterText(textField, name);
await pumpAndSettle(const Duration(milliseconds: 300));
await pumpAndSettle();
if (enter) {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
await pumpAndSettle();
} else {
await tapButton(
find.descendant(
@@ -544,11 +546,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await enterText(textField, name);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> tapChecklistNewTaskButton() async {
await tapButton(find.byType(ChecklistNewTaskButton));
await pumpAndSettle();
}
Future<void> checkChecklistTask({required int index}) async {
@@ -901,9 +899,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> assertRowCountInGridPage(int num) async {
final text = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == rowCountString(num),
);
final text = find.text('${rowCountString()} $num',findRichText: true);
expect(text, findsOneWidget);
}
@@ -1294,12 +1290,72 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cards.at(index));
}
void assertEventEditorOpen() {
expect(find.byType(CalendarEventEditor), findsOneWidget);
}
Future<void> dismissEventEditor() async {
await simulateKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> editEventTitle(String title) async {
final textField = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byType(FlowyTextField),
);
await enterText(textField, title);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> openEventToRowDetailPage() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s,
),
);
await tapButton(button);
}
Future<void> deleteEventFromEventEditor() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
),
);
await tapButton(button);
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
}
Future<void> openUnscheduledEventsPopup() async {
final button = find.byType(UnscheduledEventsButton);
await tapButton(button);
}
void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) {
expect(find.byType(UnscheduleEventsList), matcher);
if (matcher != findsNothing) {
expect(
find.byType(UnscheduledEventCell),
findsNWidgets(numUnscheduledEvents),
);
}
}
Future<void> clickUnscheduledEvent() async {
final unscheduledEvent = find.byType(UnscheduledEventCell);
await tapButton(unscheduledEvent);
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
@@ -174,4 +175,34 @@ class EditorOperations {
);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
}
/// hover and click on the + button beside the block component.
Future<void> hoverAndClickOptionAddButton(
Path path,
bool withModifiedKey, // alt on windows or linux, option on macos
) async {
final optionAddButton = find.byWidgetPredicate(
(widget) =>
widget is BlockComponentActionWrapper &&
widget.node.path.equals(path),
);
await tester.hoverOnWidget(
optionAddButton,
onHover: () async {
if (withModifiedKey) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
}
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is BlockAddButton &&
widget.blockComponentContext.node.path.equals(path),
),
);
if (withModifiedKey) {
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
}
},
);
}
}
@@ -12,8 +12,11 @@ class MyMockClient extends Mock implements http.Client {
final requestType = request.method;
final requestUri = request.url;
if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
if (requestType == 'POST' &&
requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{
'content-type': 'text/event-stream'
};
final responseBody = Stream.fromIterable([
utf8.encode(
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
@@ -51,7 +54,9 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
await onStart();
final data = chunk.trim().split('data: ');
if (data[0] != '[DONE]') {
@@ -1,9 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
@@ -72,4 +74,35 @@ extension AppFlowySettings on WidgetTester {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
// go to settings page and switch the layout direction
Future<void> switchLayoutDirectionMode(
LayoutDirection layoutDirection,
) async {
await openSettings();
await openSettingsPage(SettingsPage.appearance);
final button = find.byKey(const ValueKey('layout_direction_option_button'));
expect(button, findsOneWidget);
await tapButton(button);
switch (layoutDirection) {
case LayoutDirection.ltrLayout:
final ltrButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_ltr.tr(),
);
await tapButton(ltrButton);
break;
case LayoutDirection.rtlLayout:
final rtlButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_rtl.tr(),
);
await tapButton(rtlButton);
break;
}
// tap anywhere to close the settings page
await tapAt(Offset.zero);
await pumpAndSettle();
}
}
+16
View File
@@ -38,4 +38,20 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
installer.aggregate_targets.each do |target|
target.xcconfigs.each do |variant, xcconfig|
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
end
end
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
xcconfig_path = config.base_configuration_reference.real_path
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
end
end
end
end
@@ -20,11 +20,15 @@ final class DateCellBackendService {
Future<Either<Unit, FlowyError>> update({
DateTime? date,
String? time,
DateTime? endDate,
String? endTime,
required includeTime,
required isRange,
}) {
final payload = DateChangesetPB.create()
..cellId = cellId
..includeTime = includeTime;
..includeTime = includeTime
..isRange = isRange;
if (date != null) {
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
@@ -33,6 +37,13 @@ final class DateCellBackendService {
if (time != null) {
payload.time = time;
}
if (endDate != null) {
final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;
payload.endDate = Int64(dateTimestamp);
}
if (endTime != null) {
payload.endTime = endTime;
}
return DatabaseEventUpdateDateCell(payload).send();
}
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -29,8 +31,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
didReceiveFieldUpdate: (field) {
emit(state.copyWith(field: cellContext.field));
},
onResizeStart: () {
emit(state.copyWith(resizeStart: state.width));
},
startUpdateWidth: (offset) {
final width = state.width + offset;
final width = max(offset + state.resizeStart, 50).toDouble();
emit(state.copyWith(width: width));
},
endUpdateWidth: () {
@@ -66,6 +71,7 @@ class FieldCellEvent with _$FieldCellEvent {
const factory FieldCellEvent.initial() = _InitialCell;
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
const factory FieldCellEvent.onResizeStart() = _OnResizeStart;
const factory FieldCellEvent.startUpdateWidth(double offset) =
_StartUpdateWidth;
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
@@ -77,11 +83,13 @@ class FieldCellState with _$FieldCellState {
required String viewId,
required FieldPB field,
required double width,
required double resizeStart,
}) = _FieldCellState;
factory FieldCellState.initial(FieldContext cellContext) => FieldCellState(
viewId: cellContext.viewId,
field: cellContext.field,
width: cellContext.field.width.toDouble(),
resizeStart: 0,
);
}
@@ -382,8 +382,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
if (group.isCheck) {
widget =
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
widget = const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
);
} else {
widget = const FlowySvg(FlowySvgs.uncheck_s);
}
@@ -63,6 +63,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date) async {
await _createEvent(date);
},
newEventPopupDisplayed: () {
emit(state.copyWith(editingEvent: null));
},
moveEvent: (CalendarDayEvent event, DateTime date) async {
await _moveEvent(event, date);
},
@@ -378,6 +381,10 @@ class CalendarEvent with _$CalendarEvent {
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveNewEvent;
// Called after creating a new event
const factory CalendarEvent.newEventPopupDisplayed() =
_NewEventPopupDisplayed;
// Called when receive a new event
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarDayEvent> event,
@@ -0,0 +1,82 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'calendar_event_editor_bloc.freezed.dart';
class CalendarEventEditorBloc
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final RowBackendService _rowService;
CalendarEventEditorBloc({
required this.rowController,
required this.layoutSettings,
}) : _rowService = RowBackendService(viewId: rowController.viewId),
super(CalendarEventEditorState.initial()) {
on<CalendarEventEditorEvent>((event, emit) async {
await event.when(
initial: () {
_startListening();
final cells = rowController.loadData();
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(
cells.values.toList(),
),
);
}
},
didReceiveCellDatas: (cells) {
emit(state.copyWith(cells: cells));
},
delete: () async {
final result = await _rowService.deleteRow(rowController.rowId);
result.fold((l) => null, (err) => Log.error(err));
},
);
});
}
void _startListening() {
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(cells.values.toList()),
);
}
},
);
}
@override
Future<void> close() async {
rowController.dispose();
return super.close();
}
}
@freezed
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
const factory CalendarEventEditorEvent.initial() = _Initial;
const factory CalendarEventEditorEvent.didReceiveCellDatas(
List<DatabaseCellContext> cells,
) = _DidReceiveCellDatas;
const factory CalendarEventEditorEvent.delete() = _Delete;
}
@freezed
class CalendarEventEditorState with _$CalendarEventEditorState {
const factory CalendarEventEditorState({
required List<DatabaseCellContext> cells,
}) = _CalendarEventEditorState;
factory CalendarEventEditorState.initial() =>
CalendarEventEditorState(cells: List.empty());
}
@@ -58,9 +58,12 @@ class UnscheduleEventsBloc
);
},
didReceiveEvent: (CalendarEventPB event) {
final events = [...state.allEvents, event];
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
@@ -1,13 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../application/calendar_bloc.dart';
@@ -18,7 +19,7 @@ class CalendarDayCard extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final RowCache _rowCache;
final RowCache rowCache;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
@@ -28,18 +29,21 @@ class CalendarDayCard extends StatelessWidget {
required this.isInMonth,
required this.date,
required this.onCreateEvent,
required RowCache rowCache,
required this.rowCache,
required this.events,
Key? key,
}) : _rowCache = rowCache,
super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
Color backgroundColor = Theme.of(context).colorScheme.surface;
if (!isInMonth) {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
Color backgroundColor = Colors.transparent;
if (date.isWeekend) {
backgroundColor = AFThemeExtension.of(context).calendarWeekendBGColor;
}
final hoverBackgroundColor =
Theme.of(context).brightness == Brightness.light
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
@@ -57,14 +61,14 @@ class CalendarDayCard extends StatelessWidget {
),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight),
const VSpace(6.0),
// List of cards or empty space
if (events.isNotEmpty)
_EventList(
events: events,
viewId: viewId,
rowCache: _rowCache,
rowCache: rowCache,
constraints: constraints,
),
],
@@ -79,34 +83,29 @@ class CalendarDayCard extends StatelessWidget {
DragTarget<CalendarDayEvent>(
builder: (context, candidate, __) {
return Stack(
fit: StackFit.expand,
children: [
if (candidate.isNotEmpty)
Container(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
Container(
width: double.infinity,
height: double.infinity,
color:
candidate.isEmpty ? null : hoverBackgroundColor,
padding: const EdgeInsets.only(top: 5.0),
child: child,
)
),
if (candidate.isEmpty)
NewEventButton(onCreate: () => onCreateEvent(date)),
],
);
},
onWillAccept: (CalendarDayEvent? event) {
if (event == null) {
return false;
}
return !isSameDay(event.date, date);
},
onAccept: (CalendarDayEvent event) {
if (event.date == date) {
return;
}
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
@@ -143,7 +142,7 @@ class _Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
@@ -166,7 +165,7 @@ class NewEventButton extends StatelessWidget {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: FlowyIconButton(
onPressed: onCreate,
iconPadding: EdgeInsets.zero,
@@ -174,6 +173,35 @@ class NewEventButton extends StatelessWidget {
fillColor: Theme.of(context).colorScheme.background,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s5Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
),
),
);
},
@@ -195,37 +223,49 @@ class _DayBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
Color monthTextColor = Theme.of(context).colorScheme.onBackground;
final String monthString =
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
final String dayString = date.day.toString();
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
monthTextColor = Theme.of(context).disabledColor;
}
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
width: isToday ? 26 : null,
height: isToday ? 26 : null,
padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
color: dayTextColor,
return SizedBox(
height: 18,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (date.day == 1)
FlowyText.medium(
monthString,
fontSize: 11,
color: monthTextColor,
),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: BorderRadius.circular(10),
),
width: isToday ? 18 : null,
height: isToday ? 18 : null,
// padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
fontSize: 11,
color: dayTextColor,
),
),
),
),
],
],
),
);
}
}
@@ -246,20 +286,26 @@ class _EventList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
return Flexible(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
itemBuilder: (BuildContext context, int index) {
final autoEdit =
editingEvent?.event?.eventId == events[index].eventId;
return EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
autoEdit: autoEdit,
);
},
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
@@ -1,47 +1,73 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.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 '../application/calendar_bloc.dart';
import 'calendar_page.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatelessWidget {
class EventCard extends StatefulWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
final bool autoEdit;
const EventCard({
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
super.key,
});
@override
State<EventCard> createState() => _EventCardState();
}
class _EventCardState extends State<EventCard> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
if (widget.autoEdit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_popoverController.show();
context
.read<CalendarBloc>()
.add(const CalendarEvent.newEventPopupDisplayed());
});
}
}
@override
Widget build(BuildContext context) {
final rowInfo = rowCache.getRow(event.eventId);
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
if (rowInfo == null) {
return const SizedBox.shrink();
}
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
rowCache.cellCache,
widget.rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
@@ -49,24 +75,22 @@ class EventCard extends StatelessWidget {
final card = RowCard<CalendarDayEvent>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
rowMeta: rowInfo!.rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: event,
key: ValueKey(widget.event.eventId),
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event.event,
viewId: viewId,
rowCache: rowCache,
),
openCard: (_) => _popoverController.show(),
styleConfiguration: RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(6),
hoverStyle: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
: const Color(0x0FEFF4FB),
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
),
@@ -76,28 +100,76 @@ class EventCard extends StatelessWidget {
);
final decoration = BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s6Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
);
return Draggable<CalendarDayEvent>(
data: event,
data: widget.event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth - 16.0,
maxWidth: widget.constraints.maxWidth - 8.0,
),
child: DecoratedBox(
decoration: decoration.copyWith(
color: AFThemeExtension.of(context).lightGreyHover,
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
child: card,
),
),
child: DecoratedBox(
decoration: decoration,
child: card,
child: AppFlowyPopover(
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.rightWithCenterAligned,
controller: _popoverController,
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),
asBarrier: true,
margin: EdgeInsets.zero,
offset: const Offset(10.0, 0),
popupBuilder: (BuildContext popoverContext) {
final settings = context.watch<CalendarBloc>().state.settings.fold(
() => null,
(layoutSettings) => layoutSettings,
);
if (settings == null) {
return const SizedBox.shrink();
}
return CalendarEventEditor(
rowCache: widget.rowCache,
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
layoutSettings: settings,
);
},
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
);
}
@@ -107,13 +179,10 @@ class EventCard extends StatelessWidget {
) {
final renderHook = RowCardRenderHook<CalendarDayEvent>();
renderHook.addTextCellHook((cellData, eventData, _) {
return BlocBuilder<TextCardCellBloc, TextCardCellState>(
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final isTitle = context
.read<TextCardCellBloc>()
.cellController
.fieldInfo
.isPrimary;
final isTitle =
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
final text = isTitle && cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
@@ -127,8 +196,9 @@ class EventCard extends StatelessWidget {
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
fontSize: isTitle ? 11 : 10,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
@@ -0,0 +1,280 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.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';
class CalendarEventEditor extends StatelessWidget {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final GridCellBuilder cellBuilder;
CalendarEventEditor({
super.key,
required RowCache rowCache,
required RowMetaPB rowMeta,
required String viewId,
required this.layoutSettings,
}) : rowController = RowController(
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
),
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
@override
Widget build(BuildContext context) {
return BlocProvider<CalendarEventEditorBloc>(
create: (context) => CalendarEventEditorBloc(
rowController: rowController,
layoutSettings: layoutSettings,
)..add(const CalendarEventEditorEvent.initial()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
EventEditorControls(rowController: rowController),
Flexible(
child: EventPropertyList(
dateFieldId: layoutSettings.fieldId,
cellBuilder: cellBuilder,
),
),
],
),
);
}
}
class EventEditorControls extends StatelessWidget {
final RowController rowController;
const EventEditorControls({
super.key,
required this.rowController,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context
.read<CalendarEventEditorBloc>()
.add(const CalendarEventEditorEvent.delete()),
),
const HSpace(8.0),
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.full_view_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () {
PopoverContainer.of(context).close();
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
},
),
],
),
);
}
}
class EventPropertyList extends StatelessWidget {
final String dateFieldId;
final GridCellBuilder cellBuilder;
const EventPropertyList({
super.key,
required this.dateFieldId,
required this.cellBuilder,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
builder: (context, state) {
final reorderedList = List<DatabaseCellContext>.from(state.cells)
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
final primaryCellContext =
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
final dateFieldIndex =
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
if (primaryCellContext == null || dateFieldIndex == -1) {
return const SizedBox.shrink();
}
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
final children = <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: cellBuilder.build(
primaryCellContext,
style: GridTextCellStyle(
cellPadding: EdgeInsets.zero,
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 11),
autofocus: true,
useRoundedBorder: true,
),
),
),
...reorderedList.map(
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
),
];
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 16.0),
children: children,
);
},
);
}
}
class PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
const PropertyCell({
required this.cellContext,
required this.cellBuilder,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _PropertyCellState();
}
class _PropertyCellState extends State<PropertyCell> {
@override
Widget build(BuildContext context) {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell),
);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
constraints: const BoxConstraints(minHeight: 28),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 88,
height: 28,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),
child: Row(
children: [
FlowySvg(
widget.cellContext.fieldType.icon(),
color: Theme.of(context).hintColor,
size: const Size.square(14),
),
const HSpace(4.0),
FlowyText.regular(
widget.cellContext.fieldInfo.name,
color: Theme.of(context).hintColor,
fontSize: 11,
),
],
),
),
),
const HSpace(8),
Expanded(child: gesture),
],
),
);
}
GridCellStyle? _customCellStyle(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return GridCheckboxCellStyle(
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.DateTime:
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Checklist:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.Number:
return GridNumberCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.copyURL,
GridURLCellAccessoryType.visitURL,
],
);
}
throw UnimplementedError;
}
}
@@ -2,11 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -119,19 +123,6 @@ class _CalendarPageState extends State<CalendarPage> {
);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.editingEvent != c.editingEvent,
listener: (context, state) {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editingEvent!.event!.event,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
}
},
),
BlocListener<CalendarBloc, CalendarState>(
// Event create by click the + button or double click on the
// calendar
@@ -211,40 +202,49 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _headerNavigatorBuilder(DateTime currentMonth) {
return Row(
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w500,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
],
return SizedBox(
height: 24,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w400,
fontSize: 10,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
const HSpace(6.0),
UnscheduledEventsButton(
databaseController: widget.databaseController,
),
],
),
);
}
@@ -255,8 +255,9 @@ class _CalendarPageState extends State<CalendarPage> {
return Center(
child: Padding(
padding: CalendarSize.daysOfWeekInsets,
child: FlowyText.medium(
child: FlowyText.regular(
weekDayString,
fontSize: 9,
color: Theme.of(context).hintColor,
),
),
@@ -321,3 +322,150 @@ void showEventDetails({
},
);
}
class UnscheduledEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduledEventsButton({super.key, required this.databaseController});
@override
State<UnscheduledEventsButton> createState() =>
_UnscheduledEventsButtonState();
}
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocProvider<UnscheduleEventsBloc>(
create: (_) =>
UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial()),
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
builder: (context, state) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: Corners.s6Border,
),
side:
BorderSide(color: Theme.of(context).dividerColor, width: 1),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.unscheduleEvents.isNotEmpty) {
_popoverController.show();
}
},
child: FlowyText.regular(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: 10,
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: widget.databaseController.viewId,
rowCache: widget.databaseController.rowCache,
unscheduleEvents: state.unscheduleEvents,
);
},
);
},
),
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
PopoverContainer.of(context).close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
fontSize: 11,
),
onTap: onPressed,
),
);
}
}
@@ -15,7 +15,7 @@ class CalendarSize {
static double get scrollBarSize => 8 * scale;
static double get navigatorButtonWidth => 20 * scale;
static double get navigatorButtonHeight => 25 * scale;
static double get navigatorButtonHeight => 24 * scale;
static EdgeInsets get daysOfWeekInsets =>
EdgeInsets.symmetric(vertical: 10.0 * scale);
EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale);
}
@@ -1,18 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.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_bloc/flutter_bloc.dart';
class CalendarSettingBar extends StatelessWidget {
final DatabaseController databaseController;
@@ -28,8 +16,6 @@ class CalendarSettingBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
@@ -38,148 +24,3 @@ class CalendarSettingBar extends StatelessWidget {
);
}
}
class UnscheduleEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduleEventsButton({
required this.databaseController,
Key? key,
}) : super(key: key);
@override
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
late final PopoverController _popoverController;
late final UnscheduleEventsBloc _bloc;
@override
void initState() {
super.initState();
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial());
_popoverController = PopoverController();
}
@override
dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: BlocProvider.value(
value: _bloc,
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
buildWhen: (previous, current) =>
previous.unscheduleEvents.length !=
current.unscheduleEvents.length,
builder: (context, state) {
return FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: FontSizes.s11,
fontColor: AFThemeExtension.of(context).textColor,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () => _popoverController.show(),
);
},
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: _bloc.viewId,
rowCache: _bloc.rowCache,
controller: _popoverController,
unscheduleEvents: _bloc.state.unscheduleEvents,
);
},
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final PopoverController controller;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.controller,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}
@@ -5,10 +5,10 @@ import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -410,14 +410,21 @@ class _GridFooter extends StatelessWidget {
builder: (context, rowCount) {
return Padding(
padding: GridSize.contentInsets,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
FlowyText.medium(
rowCountString(rowCount),
color: Theme.of(context).hintColor,
),
],
child: RichText(
text: TextSpan(
text: rowCountString(),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).hintColor,
),
children: [
TextSpan(
text: ' $rowCount',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: AFThemeExtension.of(context).gridRowCountColor,
),
),
],
),
),
);
},
@@ -425,6 +432,6 @@ class _GridFooter extends StatelessWidget {
}
}
String rowCountString(int count) {
return '${LocaleKeys.grid_row_count.tr()} : $count';
String rowCountString() {
return '${LocaleKeys.grid_row_count.tr()} :';
}
@@ -125,10 +125,15 @@ class _DragToExpandLine extends StatelessWidget {
onTap: () {},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragStart: (details) {
context
.read<FieldCellBloc>()
.add(const FieldCellEvent.onResizeStart());
},
onHorizontalDragUpdate: (value) {
context
.read<FieldCellBloc>()
.add(FieldCellEvent.startUpdateWidth(value.delta.dx));
.add(FieldCellEvent.startUpdateWidth(value.localPosition.dx));
},
onHorizontalDragEnd: (end) {
context
@@ -52,8 +52,13 @@ class _FieldEditorState extends State<FieldEditor> {
@override
Widget build(BuildContext context) {
final bool requireSpace = widget.onDeleted != null ||
widget.onHidden != null ||
!widget.typeOptionLoader.field.isPrimary;
final List<Widget> children = [
FieldNameTextField(popoverMutex: popoverMutex),
if (requireSpace) const VSpace(4),
if (widget.onDeleted != null) _addDeleteFieldButton(),
if (widget.onHidden != null) _addHideFieldButton(),
if (!widget.typeOptionLoader.field.isPrimary)
@@ -200,6 +205,17 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
},
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
focusNode.dispose();
super.dispose();
}
}
class _DeleteFieldButton extends StatelessWidget {
@@ -306,7 +306,6 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: FlowyTextField(
autoClearWhenDone: true,
maxLength: 30,
text: text,
focusNode: _focusNode,
onCanceled: () {
@@ -325,4 +324,15 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
},
);
}
@override
void dispose() {
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
_focusNode.dispose();
super.dispose();
}
}
@@ -257,7 +257,10 @@ class RowContent extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _makeCells(context, state.cellByFieldId),
children: [
..._makeCells(context, state.cellByFieldId),
_finalCellDecoration(context),
],
),
);
},
@@ -302,6 +305,23 @@ class RowContent extends StatelessWidget {
},
).toList();
}
Widget _finalCellDecoration(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.basic,
child: Container(
width: GridSize.trailHeaderPadding,
padding: GridSize.headerContentInsets,
constraints: const BoxConstraints(minHeight: 46),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: const SizedBox.shrink(),
),
);
}
}
class RegionStateNotifier extends ChangeNotifier {
@@ -161,6 +161,7 @@ enum AddButtonAction {
case AddButtonAction.board:
return FlowySvgs.board_s;
case AddButtonAction.calendar:
return FlowySvgs.date_s;
case AddButtonAction.grid:
return FlowySvgs.grid_s;
}
@@ -1,77 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'checkbox_card_cell_bloc.freezed.dart';
class CheckboxCardCellBloc
extends Bloc<CheckboxCardCellEvent, CheckboxCardCellState> {
final CheckboxCellController cellController;
void Function()? _onCellChangedFn;
CheckboxCardCellBloc({
required this.cellController,
}) : super(CheckboxCardCellState.initial(cellController)) {
on<CheckboxCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(CheckboxCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class CheckboxCardCellEvent with _$CheckboxCardCellEvent {
const factory CheckboxCardCellEvent.initial() = _InitialCell;
const factory CheckboxCardCellEvent.select() = _Selected;
const factory CheckboxCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class CheckboxCardCellState with _$CheckboxCardCellState {
const factory CheckboxCardCellState({
required bool isSelected,
}) = _CheckboxCellState;
factory CheckboxCardCellState.initial(TextCellController context) {
return CheckboxCardCellState(
isSelected: _isSelected(context.getCellData()),
);
}
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
}
@@ -1,90 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'date_card_cell_bloc.freezed.dart';
class DateCardCellBloc extends Bloc<DateCardCellEvent, DateCardCellState> {
final DateCellController cellController;
void Function()? _onCellChangedFn;
DateCardCellBloc({required this.cellController})
: super(DateCardCellState.initial(cellController)) {
on<DateCardCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
emit(
state.copyWith(
data: cellData,
dateStr: _dateStrFromCellData(cellData),
),
);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(DateCardCellEvent.didReceiveCellUpdate(data));
}
}),
);
}
}
@freezed
class DateCardCellEvent with _$DateCardCellEvent {
const factory DateCardCellEvent.initial() = _InitialCell;
const factory DateCardCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
}
@freezed
class DateCardCellState with _$DateCardCellState {
const factory DateCardCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldInfo fieldInfo,
}) = _DateCardCellState;
factory DateCardCellState.initial(DateCellController context) {
final cellData = context.getCellData();
return DateCardCellState(
fieldInfo: context.fieldInfo,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);
}
}
String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = "";
if (cellData != null) {
if (cellData.includeTime) {
dateStr = '${cellData.date} ${cellData.time}';
} else {
dateStr = cellData.date;
}
}
return dateStr;
}
@@ -1,68 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'number_card_cell_bloc.freezed.dart';
class NumberCardCellBloc
extends Bloc<NumberCardCellEvent, NumberCardCellState> {
final NumberCellController cellController;
void Function()? _onCellChangedFn;
NumberCardCellBloc({
required this.cellController,
}) : super(NumberCardCellState.initial(cellController)) {
on<NumberCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class NumberCardCellEvent with _$NumberCardCellEvent {
const factory NumberCardCellEvent.initial() = _InitialCell;
const factory NumberCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class NumberCardCellState with _$NumberCardCellState {
const factory NumberCardCellState({
required String content,
}) = _NumberCardCellState;
factory NumberCardCellState.initial(TextCellController context) =>
NumberCardCellState(
content: context.getCellData() ?? "",
);
}
@@ -1,78 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_option_card_cell_bloc.freezed.dart';
class SelectOptionCardCellBloc
extends Bloc<SelectOptionCardCellEvent, SelectOptionCardCellState> {
final SelectOptionCellController cellController;
void Function()? _onCellChangedFn;
SelectOptionCardCellBloc({
required this.cellController,
}) : super(SelectOptionCardCellState.initial(cellController)) {
on<SelectOptionCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
emit(state.copyWith(selectedOptions: selectedOptions));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((selectOptionContext) {
if (!isClosed) {
add(
SelectOptionCardCellEvent.didReceiveOptions(
selectOptionContext?.selectOptions ?? [],
),
);
}
}),
);
}
}
@freezed
class SelectOptionCardCellEvent with _$SelectOptionCardCellEvent {
const factory SelectOptionCardCellEvent.initial() = _InitialCell;
const factory SelectOptionCardCellEvent.didReceiveOptions(
List<SelectOptionPB> selectedOptions,
) = _DidReceiveOptions;
}
@freezed
class SelectOptionCardCellState with _$SelectOptionCardCellState {
const factory SelectOptionCardCellState({
required List<SelectOptionPB> selectedOptions,
}) = _SelectOptionCardCellState;
factory SelectOptionCardCellState.initial(
SelectOptionCellController context,
) {
final data = context.getCellData();
return SelectOptionCardCellState(
selectedOptions: data?.selectOptions ?? [],
);
}
}
@@ -1,79 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'text_card_cell_bloc.freezed.dart';
class TextCardCellBloc extends Bloc<TextCardCellEvent, TextCardCellState> {
final TextCellController cellController;
void Function()? _onCellChangedFn;
TextCardCellBloc({
required this.cellController,
}) : super(TextCardCellState.initial(cellController)) {
on<TextCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
updateText: (text) {
if (text != state.content) {
cellController.saveCellData(text);
emit(state.copyWith(content: text));
}
},
enableEdit: (bool enabled) {
emit(state.copyWith(enableEdit: enabled));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(TextCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class TextCardCellEvent with _$TextCardCellEvent {
const factory TextCardCellEvent.initial() = _InitialCell;
const factory TextCardCellEvent.updateText(String text) = _UpdateContent;
const factory TextCardCellEvent.enableEdit(bool enabled) = _EnableEdit;
const factory TextCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class TextCardCellState with _$TextCardCellState {
const factory TextCardCellState({
required String content,
required bool enableEdit,
}) = _TextCardCellState;
factory TextCardCellState.initial(TextCellController context) =>
TextCardCellState(
content: context.getCellData() ?? "",
enableEdit: false,
);
}
@@ -1,80 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'timestamp_card_cell_bloc.freezed.dart';
class TimestampCardCellBloc
extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
final TimestampCellController cellController;
void Function()? _onCellChangedFn;
TimestampCardCellBloc({required this.cellController})
: super(TimestampCardCellState.initial(cellController)) {
on<TimestampCardCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
emit(
state.copyWith(
data: cellData,
dateStr: cellData?.dateTime ?? "",
),
);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(TimestampCardCellEvent.didReceiveCellUpdate(data));
}
}),
);
}
}
@freezed
class TimestampCardCellEvent with _$TimestampCardCellEvent {
const factory TimestampCardCellEvent.initial() = _InitialCell;
const factory TimestampCardCellEvent.didReceiveCellUpdate(
TimestampCellDataPB? data,
) = _DidReceiveCellUpdate;
}
@freezed
class TimestampCardCellState with _$TimestampCardCellState {
const factory TimestampCardCellState({
required TimestampCellDataPB? data,
required String dateStr,
required FieldInfo fieldInfo,
}) = _TimestampCardCellState;
factory TimestampCardCellState.initial(TimestampCellController context) {
final cellData = context.getCellData();
return TimestampCardCellState(
fieldInfo: context.fieldInfo,
data: cellData,
dateStr: cellData?.dateTime ?? "",
);
}
}
@@ -1,80 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'url_card_cell_bloc.freezed.dart';
class URLCardCellBloc extends Bloc<URLCardCellEvent, URLCardCellState> {
final URLCellController cellController;
void Function()? _onCellChangedFn;
URLCardCellBloc({
required this.cellController,
}) : super(URLCardCellState.initial(cellController)) {
on<URLCardCellEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
),
);
},
updateURL: (String url) {
cellController.saveCellData(url, deduplicate: true);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCardCellEvent.didReceiveCellUpdate(cellData));
}
}),
);
}
}
@freezed
class URLCardCellEvent with _$URLCardCellEvent {
const factory URLCardCellEvent.initial() = _InitialCell;
const factory URLCardCellEvent.updateURL(String url) = _UpdateURL;
const factory URLCardCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
_DidReceiveCellUpdate;
}
@freezed
class URLCardCellState with _$URLCardCellState {
const factory URLCardCellState({
required String content,
required String url,
}) = _URLCardCellState;
factory URLCardCellState.initial(URLCellController context) {
final cellData = context.getCellData();
return URLCardCellState(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
);
}
}
@@ -1,68 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/number_card_cell_bloc.dart';
import 'define.dart';
class BoardNumberCell extends StatefulWidget {
final String groupId;
final CellControllerBuilder cellControllerBuilder;
const BoardNumberCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardNumberCell> createState() => _NumberCardCellState();
}
class _NumberCardCellState extends State<BoardNumberCell> {
late NumberCardCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCardCellBloc(cellController: cellController)
..add(const NumberCardCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding,
),
child: FlowyText.medium(
state.content,
fontSize: 14,
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}
@@ -1,11 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/checkbox_card_cell_bloc.dart';
import 'card_cell.dart';
class CheckboxCardCell extends CardCell {
@@ -17,18 +16,18 @@ class CheckboxCardCell extends CardCell {
}) : super(key: key);
@override
State<CheckboxCardCell> createState() => _CheckboxCardCellState();
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCardCellState extends State<CheckboxCardCell> {
late CheckboxCardCellBloc _cellBloc;
class _CheckboxCellState extends State<CheckboxCardCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCardCellBloc(cellController: cellController);
_cellBloc.add(const CheckboxCardCellEvent.initial());
_cellBloc = CheckboxCellBloc(cellController: cellController);
_cellBloc.add(const CheckboxCellEvent.initial());
super.initState();
}
@@ -36,7 +35,7 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCardCellBloc, CheckboxCardCellState>(
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
@@ -55,8 +54,8 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCardCellBloc>()
.add(const CheckboxCardCellEvent.select()),
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
),
);
@@ -12,17 +12,17 @@ class ChecklistCardCell extends CardCell {
: super(key: key);
@override
State<ChecklistCardCell> createState() => _ChecklistCardCellState();
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCardCellState extends State<ChecklistCardCell> {
late ChecklistCardCellBloc _cellBloc;
class _ChecklistCellState extends State<ChecklistCardCell> {
late ChecklistCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@@ -31,7 +31,7 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return const SizedBox.shrink();
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/date_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -18,19 +18,19 @@ class DateCardCell<CustomCardData> extends CardCell {
}) : super(key: key);
@override
State<DateCardCell> createState() => _DateCardCellState();
State<DateCardCell> createState() => _DateCellState();
}
class _DateCardCellState extends State<DateCardCell> {
late DateCardCellBloc _cellBloc;
class _DateCellState extends State<DateCardCell> {
late DateCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCardCellBloc(cellController: cellController)
..add(const DateCardCellEvent.initial());
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState();
}
@@ -38,7 +38,7 @@ class _DateCardCellState extends State<DateCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCardCellBloc, DateCardCellState>(
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/number_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -27,19 +27,19 @@ class NumberCardCell<CustomCardData>
}) : super(key: key, style: style, cardData: cardData);
@override
State<NumberCardCell> createState() => _NumberCardCellState();
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCardCellState extends State<NumberCardCell> {
late NumberCardCellBloc _cellBloc;
class _NumberCellState extends State<NumberCardCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCardCellBloc(cellController: cellController)
..add(const NumberCardCellEvent.initial());
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
super.initState();
}
@@ -47,14 +47,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
buildWhen: (previous, current) => previous.content != current.content,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) =>
previous.cellContent != current.cellContent,
builder: (context, state) {
if (state.content.isEmpty) {
if (state.cellContent.isEmpty) {
return const SizedBox();
} else {
final Widget? custom = widget.renderHook?.call(
state.content,
state.cellContent,
widget.cardData,
context,
);
@@ -69,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
vertical: CardSizes.cardCellVPadding,
),
child: FlowyText.medium(
state.content,
state.cellContent,
fontSize: widget.style?.fontSize ?? 14,
),
),
@@ -1,12 +1,13 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/select_option_card_cell_bloc.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {}
@@ -29,11 +30,11 @@ class SelectOptionCardCell<CustomCardData>
}) : super(key: key, cardData: cardData);
@override
State<SelectOptionCardCell> createState() => _SelectOptionCardCellState();
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
late SelectOptionCardCellBloc _cellBloc;
class _SelectOptionCellState extends State<SelectOptionCardCell> {
late SelectOptionCellBloc _cellBloc;
late PopoverController _popover;
@override
@@ -41,8 +42,8 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCardCellBloc(cellController: cellController)
..add(const SelectOptionCardCellEvent.initial());
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
super.initState();
}
@@ -50,7 +51,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCardCellBloc, SelectOptionCardCellState>(
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cell_builder.dart';
import '../bloc/text_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -30,11 +30,11 @@ class TextCardCell<CustomCardData>
}) : super(key: key, style: style, cardData: cardData);
@override
State<TextCardCell> createState() => _TextCardCellState();
State<TextCardCell> createState() => _TextCellState();
}
class _TextCardCellState extends State<TextCardCell> {
late TextCardCellBloc _cellBloc;
class _TextCellState extends State<TextCardCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
final focusNode = SingleListenerFocusNode();
@@ -43,8 +43,8 @@ class _TextCardCellState extends State<TextCardCell> {
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCardCellBloc(cellController: cellController)
..add(const TextCardCellEvent.initial());
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
@@ -58,7 +58,7 @@ class _TextCardCellState extends State<TextCardCell> {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
_cellBloc.add(const TextCardCellEvent.enableEdit(false));
_cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
@@ -75,7 +75,7 @@ class _TextCardCellState extends State<TextCardCell> {
focusNode.requestFocus();
});
}
_cellBloc.add(TextCardCellEvent.enableEdit(isEditing));
_cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@@ -89,13 +89,13 @@ class _TextCardCellState extends State<TextCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCardCellBloc, TextCardCellState>(
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<TextCardCellBloc, TextCardCellState>(
child: BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (previous, current) {
if (previous.content != current.content &&
_controller.text == current.content &&
@@ -137,7 +137,7 @@ class _TextCardCellState extends State<TextCardCell> {
}
Future<void> focusChanged() async {
_cellBloc.add(TextCardCellEvent.updateText(_controller.text));
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
@override
@@ -156,7 +156,7 @@ class _TextCardCellState extends State<TextCardCell> {
}
}
Widget _buildText(TextCardCellState state) {
Widget _buildText(TextCellState state) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding,
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,19 +18,19 @@ class TimestampCardCell<CustomCardData> extends CardCell {
}) : super(key: key);
@override
State<TimestampCardCell> createState() => _TimestampCardCellState();
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCardCellState extends State<TimestampCardCell> {
late TimestampCardCellBloc _cellBloc;
class _TimestampCellState extends State<TimestampCardCell> {
late TimestampCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCardCellBloc(cellController: cellController)
..add(const TimestampCardCellEvent.initial());
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@@ -38,7 +38,7 @@ class _TimestampCardCellState extends State<TimestampCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/url_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -24,18 +24,18 @@ class URLCardCell<CustomCardData>
}) : super(key: key, style: style);
@override
State<URLCardCell> createState() => _URLCardCellState();
State<URLCardCell> createState() => _URLCellState();
}
class _URLCardCellState extends State<URLCardCell> {
late URLCardCellBloc _cellBloc;
class _URLCellState extends State<URLCardCell> {
late URLCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCardCellBloc(cellController: cellController);
_cellBloc.add(const URLCardCellEvent.initial());
_cellBloc = URLCellBloc(cellController: cellController);
_cellBloc.add(const URLCellEvent.initial());
super.initState();
}
@@ -43,7 +43,7 @@ class _URLCardCellState extends State<URLCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<URLCardCellBloc, URLCardCellState>(
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
@@ -66,7 +66,10 @@ class CardAccessoryContainer extends StatelessWidget {
return Container(
clipBehavior: Clip.hardEdge,
decoration: decoration,
child: child,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: child,
),
);
}
}
@@ -143,7 +143,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
),
);
},
icon: visibleIcon.padding(all: 6.0),
icon: visibleIcon.padding(all: 4.0),
),
onTap: () => _popoverController.show(),
).padding(horizontal: 6.0),
@@ -67,6 +67,7 @@ class GridCellBuilder {
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Number:
@@ -246,7 +247,9 @@ class RequestFocusListener extends ChangeNotifier {
}
}
abstract class GridCellStyle {}
abstract class GridCellStyle {
const GridCellStyle();
}
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;
@@ -4,4 +4,5 @@ export 'date_cell/date_cell.dart';
export 'number_cell/number_cell.dart';
export 'select_option_cell/select_option_cell.dart';
export 'text_cell/text_cell.dart';
export 'timestamp_cell/timestamp_cell.dart';
export 'url_cell/url_cell.dart';
@@ -1,13 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'checkbox_cell_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridCheckboxCellStyle extends GridCellStyle {
EdgeInsets? cellPadding;
@@ -44,10 +43,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
void initState() {
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(
service: CellBackendService(),
cellController: cellController,
)..add(const CheckboxCellEvent.initial());
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
super.initState();
}
@@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@@ -11,7 +10,6 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
void Function()? _onCellChangedFn;
CheckboxCellBloc({
required CellBackendService service,
required this.cellController,
}) : super(CheckboxCellState.initial(cellController)) {
on<CheckboxCellEvent>(
@@ -20,12 +18,12 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
initial: () {
_startListening();
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
);
},
);
@@ -73,5 +71,6 @@ class CheckboxCellState with _$CheckboxCellState {
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
}
@@ -10,17 +10,33 @@ import 'checklist_cell_bloc.dart';
import 'checklist_cell_editor.dart';
import 'checklist_progress_bar.dart';
class ChecklistCellStyle extends GridCellStyle {
String placeholder;
EdgeInsets? cellPadding;
ChecklistCellStyle({
required this.placeholder,
this.cellPadding,
});
}
class GridChecklistCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
GridChecklistCell({required this.cellControllerBuilder, Key? key})
: super(key: key);
late final ChecklistCellStyle? cellStyle;
GridChecklistCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
cellStyle = style as ChecklistCellStyle?;
}
@override
GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
}
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late ChecklistCardCellBloc _cellBloc;
late ChecklistCellBloc _cellBloc;
late final PopoverController _popover;
@override
@@ -28,7 +44,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@@ -53,15 +69,22 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
);
},
onClose: () => widget.onCellFocus.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return const SizedBox.shrink();
}
return ChecklistProgressBar(percent: state.percent);
},
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding:
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return FlowyText.medium(
widget.cellStyle?.placeholder ?? "",
color: Theme.of(context).hintColor,
);
}
return ChecklistProgressBar(percent: state.percent);
},
),
),
),
),
@@ -8,12 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'checklist_cell_bloc.freezed.dart';
class ChecklistCardCellBloc
extends Bloc<ChecklistCellEvent, ChecklistCellState> {
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final ChecklistCellController cellController;
final ChecklistCellBackendService _checklistCellSvc;
void Function()? _onCellChangedFn;
ChecklistCardCellBloc({
ChecklistCellBloc({
required this.cellController,
}) : _checklistCellSvc = ChecklistCellBackendService(
viewId: cellController.viewId,
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
@@ -30,13 +32,19 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
/// Focus node for the new task text field
late final FocusNode newTaskFocusNode;
/// A flag that determines whether the new task text field is visible
bool _isAddingNewTask = false;
@override
void initState() {
super.initState();
newTaskFocusNode = FocusNode();
newTaskFocusNode = FocusNode(
onKey: (node, event) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
node.unfocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
);
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
..add(const ChecklistCellEditorEvent.initial());
}
@@ -48,59 +56,35 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
listener: (context, state) {
if (state.allOptions.isEmpty) {
setState(() => _isAddingNewTask = true);
newTaskFocusNode.requestFocus();
}
},
builder: (context, state) {
return Focus(
onKey: (node, event) {
// don't hide new task text field if there are no tasks at all
if (state.allOptions.isNotEmpty &&
event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
setState(() {
_isAddingNewTask = false;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: CustomScrollView(
shrinkWrap: true,
physics: StyledScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.allOptions.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
percent: state.percent,
),
),
),
),
ChecklistItemList(
options: state.allOptions,
newTaskFocusNode: newTaskFocusNode,
isAddingNewTask: _isAddingNewTask,
onUpdateTask: () => setState(() {
_isAddingNewTask = true;
newTaskFocusNode.requestFocus();
}),
),
const SliverToBoxAdapter(
child: TypeOptionSeparator(spacing: 0.0),
),
SliverToBoxAdapter(
child: ChecklistNewTaskButton(
onTap: () => setState(() => _isAddingNewTask = true),
),
),
],
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.allOptions.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
percent: state.percent,
),
),
),
ChecklistItemList(
options: state.allOptions,
onUpdateTask: () => newTaskFocusNode.requestFocus(),
),
if (state.allOptions.isNotEmpty)
const TypeOptionSeparator(spacing: 0.0),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: NewTaskItem(focusNode: newTaskFocusNode),
),
],
);
},
),
@@ -118,16 +102,12 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
/// a new task if `isAddingNewTask` is true
class ChecklistItemList extends StatefulWidget {
final List<ChecklistSelectOption> options;
final FocusNode newTaskFocusNode;
final bool isAddingNewTask;
final VoidCallback onUpdateTask;
const ChecklistItemList({
super.key,
required this.options,
required this.onUpdateTask,
required this.isAddingNewTask,
required this.newTaskFocusNode,
});
@override
@@ -137,32 +117,28 @@ class ChecklistItemList extends StatefulWidget {
class _ChecklistItemListState extends State<ChecklistItemList> {
@override
Widget build(BuildContext context) {
final itemList = [
const VSpace(6.0),
...widget.options.mapIndexed(
(index, option) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: ChecklistItem(
if (widget.options.isEmpty) {
return const SizedBox.shrink();
}
final itemList = widget.options
.mapIndexed(
(index, option) => ChecklistItem(
option: option,
onSubmitted:
index == widget.options.length - 1 ? widget.onUpdateTask : null,
key: ValueKey(option.data.id),
// only allow calling the callback for the last task in the list
),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: widget.isAddingNewTask
? NewTaskItem(focusNode: widget.newTaskFocusNode)
: const SizedBox.shrink(),
),
const VSpace(6.0),
];
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => itemList[index],
childCount: itemList.length,
)
.toList();
return Flexible(
child: ListView.separated(
itemBuilder: (context, index) => itemList[index],
separatorBuilder: (context, index) => const VSpace(4),
itemCount: itemList.length,
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8.0),
),
);
}
@@ -187,6 +163,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
late final TextEditingController _textController;
late final FocusNode _focusNode;
bool _isHovered = false;
Timer? _debounceOnChanged;
@override
void initState() {
@@ -249,13 +226,9 @@ class _ChecklistItemState extends State<ChecklistItem> {
),
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
),
onSubmitted: (taskDescription) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateTaskName(
widget.option.data,
taskDescription,
),
);
onChanged: _debounceOnChangedText,
onSubmitted: (description) {
_submitUpdateTaskDescription(description);
widget.onSubmitted?.call();
},
),
@@ -276,6 +249,22 @@ class _ChecklistItemState extends State<ChecklistItem> {
),
);
}
void _debounceOnChangedText(String text) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(const Duration(milliseconds: 300), () {
_submitUpdateTaskDescription(text);
});
}
void _submitUpdateTaskDescription(String description) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateTaskName(
widget.option.data,
description,
),
);
}
}
/// Creates a new task after entering the description and pressing enter.
@@ -304,19 +293,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8),
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const FlowyIconButton(
width: 32,
icon: FlowySvg(
FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
hoverColor: Colors.transparent,
),
const HSpace(8),
Expanded(
child: TextField(
focusNode: widget.focusNode,
@@ -330,7 +312,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
vertical: 6.0,
horizontal: 2.0,
),
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
hintText: LocaleKeys.grid_checklist_addNew.tr(),
),
onSubmitted: (taskDescription) {
if (taskDescription.trim().isNotEmpty) {
@@ -340,15 +322,21 @@ class _NewTaskItemState extends State<NewTaskItem> {
),
);
}
widget.focusNode.requestFocus();
_textEditingController.clear();
},
onChanged: (value) => setState(() {}),
),
),
FlowyTextButton(
LocaleKeys.grid_checklist_submitNewTask.tr(),
fontSize: 11,
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.primaryContainer,
fillColor: _textEditingController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.primary,
hoverColor: _textEditingController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.primaryContainer,
fontColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
onPressed: () {
@@ -359,6 +347,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
),
);
}
widget.focusNode.requestFocus();
_textEditingController.clear();
},
),
@@ -367,25 +356,3 @@ class _NewTaskItemState extends State<NewTaskItem> {
);
}
}
@visibleForTesting
class ChecklistNewTaskButton extends StatelessWidget {
final VoidCallback onTap;
const ChecklistNewTaskButton({super.key, required this.onTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
margin: const EdgeInsets.all(6),
leftIcon: const FlowySvg(FlowySvgs.add_s),
onTap: onTap,
),
),
);
}
}
@@ -10,10 +10,10 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension;
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
import 'package:table_calendar/table_calendar.dart';
part 'date_cal_bloc.freezed.dart';
@@ -38,40 +38,75 @@ class DateCellCalendarBloc
await event.when(
initial: () async => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final (dateTime, time, includeTime) =
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
_dateDataFromCellData(cellData);
final endDay =
isRange == state.isRange && isRange ? endDateTime : null;
emit(
state.copyWith(
dateTime: dateTime,
time: time,
endDateTime: endDateTime,
endTime: endTime,
includeTime: includeTime,
isRange: isRange,
startDay: isRange ? dateTime : null,
endDay: endDay,
),
);
},
didReceiveTimeFormatError: (String? timeFormatError) {
emit(state.copyWith(timeFormatError: timeFormatError));
didReceiveTimeFormatError:
(String? parseTimeError, String? parseEndTimeError) {
emit(
state.copyWith(
parseTimeError: parseTimeError,
parseEndTimeError: parseEndTimeError,
),
);
},
selectDay: (date) async {
if (state.isRange) {
return;
}
await _updateDateData(date: date);
},
setIncludeTime: (includeTime) async {
await _updateDateData(includeTime: includeTime);
},
setIsRange: (isRange) async {
await _updateDateData(isRange: isRange);
},
setTime: (time) async {
await _updateDateData(time: time);
},
selectDateRange: (DateTime? start, DateTime? end) async {
if (end == null && state.startDay != null && state.endDay == null) {
final (newStart, newEnd) = state.startDay!.isBefore(start!)
? (state.startDay!, start)
: (start, state.startDay!);
emit(state.copyWith(startDay: null, endDay: null));
await _updateDateData(
date: newStart.toLocal().date,
endDate: newEnd.toLocal().date,
);
} else if (end == null) {
emit(state.copyWith(startDay: start, endDay: null));
} else {
await _updateDateData(
date: start!.toLocal().date,
endDate: end.toLocal().date,
);
}
},
setEndTime: (String endTime) async {
await _updateDateData(endTime: endTime);
},
setDateFormat: (dateFormat) async {
await _updateTypeOption(emit, dateFormat: dateFormat);
},
setTimeFormat: (timeFormat) async {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setCalFormat: (format) {
emit(state.copyWith(format: format));
},
setFocusedDay: (focusedDay) {
emit(state.copyWith(focusedDay: focusedDay));
},
clearDate: () async {
await _clearDate();
},
@@ -83,39 +118,66 @@ class DateCellCalendarBloc
Future<void> _updateDateData({
DateTime? date,
String? time,
DateTime? endDate,
String? endTime,
bool? includeTime,
bool? isRange,
}) async {
// make sure that not both date and time are updated at the same time
assert(
date == null && time == null ||
date == null && time != null ||
date != null && time == null,
!(date != null && time != null) || !(endDate != null && endTime != null),
);
// if not updating the time, use the old time in the state
final String? newTime = time ?? state.time;
DateTime? newDate = _utcToLocalAndAddCurrentTime(date);
DateTime? newDate;
if (time != null && time.isNotEmpty) {
newDate = state.dateTime ?? DateTime.now();
} else {
newDate = _utcToLocalAndAddCurrentTime(date);
}
// if not updating the time, use the old time in the state
final String? newEndTime = endTime ?? state.endTime;
DateTime? newEndDate;
if (endTime != null && endTime.isNotEmpty) {
newEndDate = state.endDateTime ?? DateTime.now();
} else {
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
}
final result = await _dateCellBackendService.update(
date: newDate,
time: newTime,
endDate: newEndDate,
endTime: newEndTime,
includeTime: includeTime ?? state.includeTime,
isRange: isRange ?? state.isRange,
);
result.fold(
(_) {
if (!isClosed && state.timeFormatError != null) {
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) {
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
);
}
},
(err) {
switch (err.code) {
case ErrorCode.InvalidDateTimeFormat:
if (isClosed) return;
if (isClosed) {
return;
}
// to determine which textfield should show error
final (startError, endError) = newDate != null
? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err));
add(
DateCellCalendarEvent.didReceiveTimeFormatError(
timeFormatPrompt(err),
startError,
endError,
),
);
break;
@@ -130,9 +192,13 @@ class DateCellCalendarBloc
final result = await _dateCellBackendService.clear();
result.fold(
(_) {
if (!isClosed) {
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
if (isClosed) {
return;
}
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
);
},
(err) => Log.error(err),
);
@@ -157,18 +223,13 @@ class DateCellCalendarBloc
}
String timeFormatPrompt(FlowyError error) {
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
switch (state.dateTypeOptionPB.timeFormat) {
case TimeFormatPB.TwelveHour:
msg = "$msg e.g. 01:00 PM";
break;
case TimeFormatPB.TwentyFourHour:
msg = "$msg e.g. 13:00";
break;
default:
break;
}
return msg;
return switch (state.dateTypeOptionPB.timeFormat) {
TimeFormatPB.TwelveHour =>
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM",
TimeFormatPB.TwentyFourHour =>
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00",
_ => "",
};
}
@override
@@ -235,19 +296,21 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
String? timeformatError,
String? parseTimeError,
String? parseEndTimeError,
) = _DidReceiveTimeFormatError;
// table calendar's UI settings
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) =
_CalendarFormat;
// date cell data is modified
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellCalendarEvent.selectDateRange(
DateTime? start,
DateTime? end,
) = _SelectDateRange;
const factory DateCellCalendarEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange;
// date field type options are modified
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
@@ -261,13 +324,24 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
@freezed
class DateCellCalendarState with _$DateCellCalendarState {
const factory DateCellCalendarState({
// the date field's type option
required DateTypeOptionPB dateTypeOptionPB,
required CalendarFormat format,
required DateTime focusedDay,
// used when selecting a date range
required DateTime? startDay,
required DateTime? endDay,
// cell data from the backend
required DateTime? dateTime,
required DateTime? endDateTime,
required String? time,
required String? endTime,
required bool includeTime,
required String? timeFormatError,
required bool isRange,
// error and hint text
required String? parseTimeError,
required String? parseEndTimeError,
required String timeHintText,
}) = _DateCellCalendarState;
@@ -275,15 +349,20 @@ class DateCellCalendarState with _$DateCellCalendarState {
DateTypeOptionPB dateTypeOptionPB,
DateCellDataPB? cellData,
) {
final (dateTime, time, includeTime) = _dateDataFromCellData(cellData);
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
_dateDataFromCellData(cellData);
return DateCellCalendarState(
dateTypeOptionPB: dateTypeOptionPB,
format: CalendarFormat.month,
focusedDay: DateTime.now(),
startDay: isRange ? dateTime : null,
endDay: isRange ? endDateTime : null,
dateTime: dateTime,
endDateTime: endDateTime,
time: time,
endTime: endTime,
includeTime: includeTime,
timeFormatError: null,
isRange: isRange,
parseTimeError: null,
parseEndTimeError: null,
timeHintText: _timeHintText(dateTypeOptionPB),
);
}
@@ -300,21 +379,31 @@ String _timeHintText(DateTypeOptionPB typeOption) {
}
}
(DateTime?, String?, bool) _dateDataFromCellData(DateCellDataPB? cellData) {
(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData(
DateCellDataPB? cellData,
) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// at their default values: empty strings and false booleans
// their default values: empty strings and false booleans
if (cellData == null) {
return (null, null, false);
return (null, null, null, null, false, false);
}
DateTime? dateTime;
String? time;
DateTime? endDateTime;
String? endTime;
if (cellData.hasTimestamp()) {
final timestamp = cellData.timestamp * 1000;
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
time = cellData.time;
if (cellData.hasEndTimestamp()) {
final endTimestamp = cellData.endTimestamp * 1000;
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
endTime = cellData.endTime;
}
}
final bool includeTime = cellData.includeTime;
final bool isRange = cellData.isRange;
return (dateTime, time, includeTime);
return (dateTime, endDateTime, time, endTime, includeTime, isRange);
}
@@ -74,7 +74,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
controller: _popover,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 520)),
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: GridDateCellText(
dateStr: state.dateStr,
@@ -79,8 +79,19 @@ class DateCellState with _$DateCellState {
}
String _dateStrFromCellData(DateCellDataPB? cellData) {
if (cellData == null || !cellData.hasTimestamp()) {
return "";
}
String dateStr = "";
if (cellData != null) {
if (cellData.isRange) {
if (cellData.includeTime) {
dateStr =
"${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}";
} else {
dateStr = "${cellData.date} → ${cellData.endDate}";
}
} else {
if (cellData.includeTime) {
dateStr = "${cellData.date} ${cellData.time}";
} else {
@@ -3,6 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
@@ -104,36 +106,24 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
cellData: widget.cellContext.getCellData(),
cellController: widget.cellContext,
)..add(const DateCellCalendarEvent.initial()),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
final List<Widget> children = [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.includeTime
? _TimeTextField(
timeStr: state.time,
popoverMutex: popoverMutex,
)
: const SizedBox.shrink(),
),
child: Padding(
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StartTextField(popoverMutex: popoverMutex),
EndTextField(popoverMutex: popoverMutex),
const DatePicker(),
const VSpace(8.0),
const TypeOptionSeparator(spacing: 8.0),
const TypeOptionSeparator(spacing: 12.0),
const EndTimeButton(),
const VSpace(4.0),
const _IncludeTimeButton(),
const TypeOptionSeparator(spacing: 8.0),
DateTypeOptionButton(popoverMutex: popoverMutex),
VSpace(GridSize.typeOptionSeparatorHeight),
const VSpace(4.0),
const ClearDateButton(),
];
return ListView.builder(
shrinkWrap: true,
controller: ScrollController(),
itemCount: children.length,
itemBuilder: (BuildContext context, int index) => children[index],
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
);
},
],
),
),
);
}
@@ -145,16 +135,73 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
}
}
class DatePicker extends StatelessWidget {
class StartTextField extends StatelessWidget {
final PopoverMutex popoverMutex;
const StartTextField({super.key, required this.popoverMutex});
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.includeTime
? _TimeTextField(
isEndTime: false,
timeStr: state.time,
popoverMutex: popoverMutex,
)
: const SizedBox.shrink(),
);
},
);
}
}
class EndTextField extends StatelessWidget {
final PopoverMutex popoverMutex;
const EndTextField({super.key, required this.popoverMutex});
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.includeTime && state.isRange
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: _TimeTextField(
isEndTime: true,
timeStr: state.endTime,
popoverMutex: popoverMutex,
),
)
: const SizedBox.shrink(),
);
},
);
}
}
class DatePicker extends StatefulWidget {
const DatePicker({super.key});
@override
State<DatePicker> createState() => _DatePickerState();
}
class _DatePickerState extends State<DatePicker> {
DateTime _focusedDay = DateTime.now();
CalendarFormat _calendarFormat = CalendarFormat.month;
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
final textStyle = Theme.of(context).textTheme.bodyMedium!;
final boxDecoration = BoxDecoration(
color: Theme.of(context).colorScheme.surface,
color: Theme.of(context).cardColor,
shape: BoxShape.circle,
);
return Padding(
@@ -162,10 +209,15 @@ class DatePicker extends StatelessWidget {
child: TableCalendar(
firstDay: kFirstDay,
lastDay: kLastDay,
focusedDay: state.focusedDay,
focusedDay: _focusedDay,
rowHeight: 26.0 + 7.0,
calendarFormat: state.format,
calendarFormat: _calendarFormat,
daysOfWeekHeight: 17.0 + 8.0,
rangeSelectionMode: state.isRange
? RangeSelectionMode.enforced
: RangeSelectionMode.disabled,
rangeStartDay: state.isRange ? state.startDay : null,
rangeEndDay: state.isRange ? state.endDay : null,
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
@@ -198,15 +250,29 @@ class DatePicker extends StatelessWidget {
),
weekendDecoration: boxDecoration,
outsideDecoration: boxDecoration,
rangeStartDecoration: boxDecoration.copyWith(
color: Theme.of(context).colorScheme.primary,
),
rangeEndDecoration: boxDecoration.copyWith(
color: Theme.of(context).colorScheme.primary,
),
defaultTextStyle: textStyle,
weekendTextStyle: textStyle,
selectedTextStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.surface,
),
rangeStartTextStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.surface,
),
rangeEndTextStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.surface,
),
todayTextStyle: textStyle,
outsideTextStyle: textStyle.copyWith(
color: Theme.of(context).disabledColor,
),
rangeHighlightColor:
Theme.of(context).colorScheme.secondaryContainer,
),
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
@@ -223,22 +289,24 @@ class DatePicker extends StatelessWidget {
);
},
),
selectedDayPredicate: (day) => isSameDay(state.dateTime, day),
selectedDayPredicate: (day) =>
state.isRange ? false : isSameDay(state.dateTime, day),
onDaySelected: (selectedDay, focusedDay) {
context.read<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
);
},
onFormatChanged: (format) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setCalFormat(format));
},
onPageChanged: (focusedDay) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setFocusedDay(focusedDay));
onRangeSelected: (start, end, focusedDay) {
context.read<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDateRange(start, end),
);
},
onFormatChanged: (calendarFormat) => setState(() {
_calendarFormat = calendarFormat;
}),
onPageChanged: (focusedDay) => setState(() {
_focusedDay = focusedDay;
}),
),
);
},
@@ -268,13 +336,57 @@ class _IncludeTimeButton extends StatelessWidget {
}
}
@visibleForTesting
class EndTimeButton extends StatelessWidget {
const EndTimeButton({super.key});
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
selector: (state) => state.isRange,
builder: (context, isRange) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: Row(
children: [
FlowySvg(
FlowySvgs.date_s,
color: Theme.of(context).iconTheme.color,
),
const HSpace(6),
FlowyText.medium(LocaleKeys.grid_field_isRange.tr()),
const Spacer(),
Toggle(
value: isRange,
onChanged: (value) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIsRange(!value)),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
),
);
},
);
}
}
class _TimeTextField extends StatefulWidget {
final bool isEndTime;
final String? timeStr;
final PopoverMutex popoverMutex;
const _TimeTextField({
required this.timeStr,
required this.popoverMutex,
required this.isEndTime,
Key? key,
}) : super(key: key);
@@ -309,27 +421,59 @@ class _TimeTextFieldState extends State<_TimeTextField> {
@override
Widget build(BuildContext context) {
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
listener: (context, state) => _textController.text = state.time ?? "",
listener: (context, state) {
if (widget.isEndTime) {
_textController.text = state.endTime ?? "";
} else {
_textController.text = state.time ?? "";
}
},
builder: (context, state) {
String text = "";
if (!widget.isEndTime && state.time != null) {
text = state.time!;
} else if (state.endTime != null) {
text = state.endTime!;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: FlowyTextField(
text: state.time ?? "",
text: text,
focusNode: _focusNode,
controller: _textController,
submitOnLeave: true,
hintText: state.timeHintText,
errorText: state.timeFormatError,
errorText: widget.isEndTime
? state.parseEndTimeError
: state.parseTimeError,
onSubmitted: (timeStr) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTime(timeStr));
if (widget.isEndTime) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setEndTime(timeStr));
} else {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTime(timeStr));
}
},
),
);
},
);
}
@override
void dispose() {
_textController.dispose();
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
_focusNode.dispose();
super.dispose();
}
}
@visibleForTesting
@@ -8,22 +8,24 @@ import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridTextCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
bool? autofocus;
double emojiFontSize;
double emojiHPadding;
bool showEmoji;
EdgeInsets? cellPadding;
final String? placeholder;
final TextStyle? textStyle;
final EdgeInsets? cellPadding;
final bool autofocus;
final double emojiFontSize;
final double emojiHPadding;
final bool showEmoji;
final bool useRoundedBorder;
GridTextCellStyle({
const GridTextCellStyle({
this.placeholder,
this.textStyle,
this.autofocus,
this.cellPadding,
this.autofocus = false,
this.showEmoji = true,
this.emojiFontSize = 16,
this.emojiHPadding = 0,
this.cellPadding,
this.useRoundedBorder = false,
});
}
@@ -38,7 +40,7 @@ class GridTextCell extends GridCellWidget {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = GridTextCellStyle();
cellStyle = const GridTextCellStyle();
}
}
@@ -55,12 +57,12 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController);
_cellBloc.add(const TextCellEvent.initial());
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
super.initState();
}
@override
@@ -94,23 +96,36 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
),
HSpace(widget.cellStyle.emojiHPadding),
Expanded(
child: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle.autofocus ?? false,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top,
bottom: GridSize.cellContentInsets.bottom,
),
border: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
),
child: widget.cellStyle.useRoundedBorder
? FlowyTextField(
controller: _controller,
textStyle: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
focusNode: focusNode,
autoFocus: widget.cellStyle.autofocus,
hintText: widget.cellStyle.placeholder,
onChanged: (text) => _cellBloc.add(
TextCellEvent.updateText(text),
),
debounceDuration: const Duration(milliseconds: 300),
)
: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle.autofocus,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top,
bottom: GridSize.cellContentInsets.bottom,
),
border: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
),
)
],
),
@@ -29,6 +29,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
didUpdateEmoji: (String emoji) {
emit(state.copyWith(emoji: emoji));
},
enableEdit: (bool enabled) {
emit(state.copyWith(enableEdit: enabled));
},
);
},
);
@@ -66,6 +69,7 @@ class TextCellEvent with _$TextCellEvent {
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
const factory TextCellEvent.updateText(String text) = _UpdateText;
const factory TextCellEvent.enableEdit(bool enabled) = _EnableEdit;
const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji;
}
@@ -74,10 +78,12 @@ class TextCellState with _$TextCellState {
const factory TextCellState({
required String content,
required String emoji,
required bool enableEdit,
}) = _TextCellState;
factory TextCellState.initial(TextCellController context) => TextCellState(
content: context.getCellData() ?? "",
emoji: context.emoji ?? "",
enableEdit: false,
);
}
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -19,13 +20,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'accessory/cell_accessory.dart';
import 'cell_builder.dart';
import 'cells/checkbox_cell/checkbox_cell.dart';
import 'cells/date_cell/date_cell.dart';
import 'cells/number_cell/number_cell.dart';
import 'cells/select_option_cell/select_option_cell.dart';
import 'cells/text_cell/text_cell.dart';
import 'cells/timestamp_cell/timestamp_cell.dart';
import 'cells/url_cell/url_cell.dart';
/// Display the row properties in a list. Only use this widget in the
/// [RowDetailPage].
@@ -251,8 +245,9 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Checklist:
return SelectOptionCellStyle(
return ChecklistCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.Number:
return GridNumberCellStyle(
@@ -34,7 +34,7 @@ class TransactionAdapter {
final DocumentService documentService;
final String documentId;
final bool _enableDebug = true;
final bool _enableDebug = false;
Future<void> apply(Transaction transaction, EditorState editorState) async {
final stopwatch = Stopwatch()..start();
@@ -119,12 +119,15 @@ extension on InsertOperation {
final parentId = node.parent?.id ??
editorState.getNodeAtPath(currentPath.parent)?.id ??
'';
var prevId = previousNode?.id ??
editorState.getNodeAtPath(currentPath.previous)?.id ??
'';
var prevId = previousNode?.id;
// if the node is the first child of the parent, then its prevId should be empty.
final isFirstChild = currentPath.previous.equals(currentPath);
if (!isFirstChild) {
prevId ??= editorState.getNodeAtPath(currentPath.previous)?.id ?? '';
}
prevId ??= '';
assert(parentId.isNotEmpty);
if (currentPath.equals(currentPath.previous) &&
!currentPath.equals([0])) {
if (isFirstChild) {
prevId = '';
} else {
assert(prevId.isNotEmpty && prevId != node.id);
@@ -75,8 +75,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
alignToolbarItem,
buildTextColorItem(),
buildHighlightColorItem(),
// TODO: enable it in version 0.3.3
// ...textDirectionItems,
customizeFontToolbarItem,
];
late final List<SelectionMenuItem> slashMenuItems;
@@ -137,6 +136,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
convertibleBlockTypes.add(ToggleListBlockKeys.type);
slashMenuItems = _customSlashMenuItems();
effectiveScrollController = widget.scrollController ?? ScrollController();
// keep the previous font style when typing new text.
AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
}
@override
@@ -153,6 +155,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final (bool autoFocus, Selection? selection) =
_computeAutoFocusParameters();
final isRTL =
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
LayoutDirection.rtlLayout;
final layoutDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
_setRTLToolbarItems(isRTL);
final editorScrollController = EditorScrollController(
editorState: widget.editorState,
shrinkWrap: widget.shrinkWrap,
@@ -168,22 +177,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
focusedSelection: selection,
// setup the theme
editorStyle: styleCustomizer.style(),
// customize the block builder
// customize the block builders
blockComponentBuilders: blockComponentBuilders,
// customize the shortcuts
characterShortcutEvents: characterShortcutEvents,
commandShortcutEvents: commandShortcutEvents,
// customize the context menu items
contextMenuItems: customContextMenuItems,
// customize the header and footer.
header: widget.header,
footer: const VSpace(200),
);
final layoutDirection =
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
LayoutDirection.rtlLayout
? TextDirection.rtl
: TextDirection.ltr;
return Center(
child: FloatingToolbar(
style: styleCustomizer.floatingToolbarStyleBuilder(),
@@ -463,4 +468,16 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
customizeShortcuts,
);
}
void _setRTLToolbarItems(bool isRTL) {
final textDirectionItemIds = textDirectionItems.map((e) => e.id);
// clear all the text direction items
toolbarItems.removeWhere(
(item) => textDirectionItemIds.contains(item.id),
);
// only show the rtl item when the layout direction is ltr.
if (isRTL) {
toolbarItems.addAll(textDirectionItems);
}
}
}
@@ -243,8 +243,10 @@ class ColorOptionAction extends PopoverActionCell {
final bgColor =
node.attributes[blockComponentBackgroundColor] as String?;
final selectedColor = bgColor?.tryToColor();
// get default background color from themeExtension
final defaultColor = AFThemeExtension.of(context).calloutBGColor;
// get default background color for callout block from themeExtension
final defaultColor = node.type == CalloutBlockKeys.type
? AFThemeExtension.of(context).calloutBGColor
: Colors.transparent;
final colors = [
// reset to default background color
FlowyColorOption(
@@ -151,14 +151,17 @@ class _AlignButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Tooltip(
message: tooltips,
child: FlowySvg(
icon,
size: const Size.square(16),
color: Colors.white,
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onTap,
child: Tooltip(
message: tooltips,
child: FlowySvg(
icon,
size: const Size.square(16),
color: Colors.white,
),
),
),
);
@@ -61,13 +61,10 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node(
// building the callout block widget
class CalloutBlockComponentBuilder extends BlockComponentBuilder {
CalloutBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
required this.defaultColor,
});
@override
final BlockComponentConfiguration configuration;
final Color defaultColor;
@override
@@ -51,13 +51,10 @@ SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
class CodeBlockComponentBuilder extends BlockComponentBuilder {
CodeBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
this.padding = const EdgeInsets.all(0),
});
@override
final BlockComponentConfiguration configuration;
final EdgeInsets padding;
@override
@@ -17,12 +17,9 @@ class DatabaseBlockKeys {
class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder {
DatabaseViewBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
});
@override
final BlockComponentConfiguration configuration;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
@@ -85,7 +82,15 @@ class _DatabaseBlockComponentWidgetState
child = Padding(
padding: padding,
child: child,
child: FocusScope(
skipTraversal: true,
onFocusChange: (value) {
if (value) {
context.read<EditorState>().selection = null;
}
},
child: child,
),
);
if (widget.showActions && widget.actionBuilder != null) {
@@ -8,26 +8,26 @@ import 'emoji_picker.dart';
/// Config for customizations
class Config {
/// Constructor
const Config(
{this.columns = 7,
this.emojiSizeMax = 32.0,
this.verticalSpacing = 0,
this.horizontalSpacing = 0,
this.initCategory = Category.RECENT,
this.bgColor = const Color(0xFFEBEFF2),
this.indicatorColor = Colors.blue,
this.iconColor = Colors.grey,
this.iconColorSelected = Colors.blue,
this.progressIndicatorColor = Colors.blue,
this.backspaceColor = Colors.blue,
this.showRecentsTab = true,
this.recentsLimit = 28,
this.noRecentsText = 'No Recents',
this.noRecentsStyle =
const TextStyle(fontSize: 20, color: Colors.black26),
this.tabIndicatorAnimDuration = kTabScrollDuration,
this.categoryIcons = const CategoryIcons(),
this.buttonMode = ButtonMode.MATERIAL,});
const Config({
this.columns = 7,
this.emojiSizeMax = 32.0,
this.verticalSpacing = 0,
this.horizontalSpacing = 0,
this.initCategory = Category.RECENT,
this.bgColor = const Color(0xFFEBEFF2),
this.indicatorColor = Colors.blue,
this.iconColor = Colors.grey,
this.iconColorSelected = Colors.blue,
this.progressIndicatorColor = Colors.blue,
this.backspaceColor = Colors.blue,
this.showRecentsTab = true,
this.recentsLimit = 28,
this.noRecentsText = 'No Recents',
this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26),
this.tabIndicatorAnimDuration = kTabScrollDuration,
this.categoryIcons = const CategoryIcons(),
this.buttonMode = ButtonMode.MATERIAL,
});
/// Number of emojis per row
final int columns;
@@ -27,14 +27,16 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
@override
void initState() {
var initCategory = widget.state.categoryEmoji.indexWhere(
(element) => element.category == widget.config.initCategory,);
(element) => element.category == widget.config.initCategory,
);
if (initCategory == -1) {
initCategory = 0;
}
_tabController = TabController(
initialIndex: initCategory,
length: widget.state.categoryEmoji.length,
vsync: this,);
initialIndex: initCategory,
length: widget.state.categoryEmoji.length,
vsync: this,
);
_pageController = PageController(initialPage: initCategory);
_emojiFocusNode.requestFocus();
@@ -72,14 +74,15 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
return Material(
type: MaterialType.transparency,
child: IconButton(
padding: const EdgeInsets.only(bottom: 2),
icon: Icon(
Icons.backspace,
color: widget.config.backspaceColor,
),
onPressed: () {
widget.state.onBackspacePressed!();
},),
padding: const EdgeInsets.only(bottom: 2),
icon: Icon(
Icons.backspace,
color: widget.config.backspaceColor,
),
onPressed: () {
widget.state.onBackspacePressed!();
},
),
);
}
return Container();
@@ -160,8 +163,12 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
: widget.state.categoryEmoji
.asMap()
.entries
.map<Widget>((item) => _buildCategory(
item.value.category, emojiSize,),)
.map<Widget>(
(item) => _buildCategory(
item.value.category,
emojiSize,
),
)
.toList(),
),
),
@@ -206,8 +213,10 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
);
}
Widget _buildButtonWidget(
{required VoidCallback onPressed, required Widget child,}) {
Widget _buildButtonWidget({
required VoidCallback onPressed,
required Widget child,
}) {
if (widget.config.buttonMode == ButtonMode.MATERIAL) {
return InkWell(
onTap: onPressed,
@@ -266,29 +275,31 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
Emoji emoji,
) {
return _buildButtonWidget(
onPressed: () {
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
},
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
emoji.emoji,
textScaleFactor: 1.0,
style: TextStyle(
fontSize: emojiSize,
backgroundColor: Colors.transparent,
),
onPressed: () {
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
},
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
emoji.emoji,
textScaleFactor: 1.0,
style: TextStyle(
fontSize: emojiSize,
backgroundColor: Colors.transparent,
),
),);
),
),
);
}
Widget _buildNoRecent() {
return Center(
child: FlowyText.regular(
child: FlowyText.regular(
widget.config.noRecentsText,
color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
fontSize: widget.config.noRecentsStyle.fontSize,
textAlign: TextAlign.center,
),);
),
);
}
}
@@ -190,30 +190,49 @@ class EmojiPickerState extends State<EmojiPicker> {
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
}
categoryEmoji.addAll([
CategoryEmoji(Category.SMILEYS,
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),),
CategoryEmoji(Category.ANIMALS,
await _getAvailableEmojis(emoji_list.animals, title: 'animals'),),
CategoryEmoji(Category.FOODS,
await _getAvailableEmojis(emoji_list.foods, title: 'foods'),),
CategoryEmoji(
Category.ACTIVITIES,
await _getAvailableEmojis(emoji_list.activities,
title: 'activities',),),
CategoryEmoji(Category.TRAVEL,
await _getAvailableEmojis(emoji_list.travel, title: 'travel'),),
CategoryEmoji(Category.OBJECTS,
await _getAvailableEmojis(emoji_list.objects, title: 'objects'),),
CategoryEmoji(Category.SYMBOLS,
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),),
CategoryEmoji(Category.FLAGS,
await _getAvailableEmojis(emoji_list.flags, title: 'flags'),)
Category.SMILEYS,
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),
),
CategoryEmoji(
Category.ANIMALS,
await _getAvailableEmojis(emoji_list.animals, title: 'animals'),
),
CategoryEmoji(
Category.FOODS,
await _getAvailableEmojis(emoji_list.foods, title: 'foods'),
),
CategoryEmoji(
Category.ACTIVITIES,
await _getAvailableEmojis(
emoji_list.activities,
title: 'activities',
),
),
CategoryEmoji(
Category.TRAVEL,
await _getAvailableEmojis(emoji_list.travel, title: 'travel'),
),
CategoryEmoji(
Category.OBJECTS,
await _getAvailableEmojis(emoji_list.objects, title: 'objects'),
),
CategoryEmoji(
Category.SYMBOLS,
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),
),
CategoryEmoji(
Category.FLAGS,
await _getAvailableEmojis(emoji_list.flags, title: 'flags'),
)
]);
}
// Get available emoji for given category title
Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map,
{required String title,}) async {
Future<List<Emoji>> _getAvailableEmojis(
Map<String, String> map, {
required String title,
}) async {
Map<String, String>? newMap;
// Get Emojis cached locally if available
@@ -236,15 +255,18 @@ class EmojiPickerState extends State<EmojiPicker> {
// Check if emoji is available on current platform
Future<Map<String, String>?> _getPlatformAvailableEmoji(
Map<String, String> emoji,) async {
Map<String, String> emoji,
) async {
if (Platform.isAndroid) {
Map<String, String>? filtered = {};
const delimiter = '|';
try {
final entries = emoji.values.join(delimiter);
final keys = emoji.keys.join(delimiter);
final result = (await platform.invokeMethod<String>('checkAvailability',
{'emojiKeys': keys, 'emojiEntries': entries},)) as String;
final result = (await platform.invokeMethod<String>(
'checkAvailability',
{'emojiKeys': keys, 'emojiEntries': entries},
)) as String;
final resultKeys = result.split(delimiter);
for (var i = 0; i < resultKeys.length; i++) {
filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
@@ -272,7 +294,9 @@ class EmojiPickerState extends State<EmojiPicker> {
// Stores filtered emoji locally for faster access next time
Future<void> _cacheFilteredEmojis(
String title, Map<String, String> emojis,) async {
String title,
Map<String, String> emojis,
) async {
final prefs = await SharedPreferences.getInstance();
final emojiJson = jsonEncode(emojis);
prefs.setString(title, emojiJson);
@@ -305,7 +329,9 @@ class EmojiPickerState extends State<EmojiPicker> {
recentEmoji.sort((a, b) => b.counter - a.counter);
// Limit entries to recentsLimit
recentEmoji = recentEmoji.sublist(
0, min(widget.config.recentsLimit, recentEmoji.length),);
0,
min(widget.config.recentsLimit, recentEmoji.length),
);
// save locally
prefs.setString('recent', jsonEncode(recentEmoji));
}
@@ -1,4 +1,3 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
@@ -0,0 +1,48 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.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:flutter/material.dart';
final customizeFontToolbarItem = ToolbarItem(
id: 'editor.font',
group: 4,
isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor) {
final selection = editorState.selection!;
final popoverController = PopoverController();
return MouseRegion(
cursor: SystemMouseCursors.click,
child: FontFamilyDropDown(
currentFontFamily: '',
offset: const Offset(0, 12),
popoverController: popoverController,
onOpen: () => keepEditorFocusNotifier.value += 1,
onClose: () => keepEditorFocusNotifier.value -= 1,
showResetButton: true,
onFontFamilyChanged: (fontFamily) async {
await popoverController.close();
try {
await editorState.formatDelta(selection, {
AppFlowyRichTextKeys.fontFamily: fontFamily,
});
} catch (e) {
Log.error('Failed to set font family: $e');
}
},
onResetFont: () async => await editorState.formatDelta(selection, {
AppFlowyRichTextKeys.fontFamily: null,
}),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: FlowySvg(
FlowySvgs.font_family_s,
size: Size.square(16.0),
color: Colors.white,
),
),
),
);
},
);
@@ -41,36 +41,6 @@ class ChangeCoverPopover extends StatefulWidget {
State<ChangeCoverPopover> createState() => _ChangeCoverPopoverState();
}
class ColorOption {
final String colorHex;
final String name;
const ColorOption({
required this.colorHex,
required this.name,
});
}
class CoverColorPicker extends StatefulWidget {
final String? selectedBackgroundColorHex;
final Color pickerBackgroundColor;
final Color pickerItemHoverColor;
final void Function(String color) onSubmittedBackgroundColorHex;
final List<ColorOption> backgroundColorOptions;
const CoverColorPicker({
super.key,
this.selectedBackgroundColorHex,
required this.pickerBackgroundColor,
required this.backgroundColorOptions,
required this.pickerItemHoverColor,
required this.onSubmittedBackgroundColorHex,
});
@override
State<CoverColorPicker> createState() => _CoverColorPickerState();
}
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
bool isAddingImage = false;
@@ -81,7 +51,15 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
editorState: widget.editorState,
node: widget.node,
)..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()),
child: BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
child: BlocConsumer<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
listener: (context, state) {
if (state is Loaded && state.selectLatestImage) {
widget.onCoverChanged(
CoverType.file,
state.imageNames.last,
);
}
},
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(12),
@@ -91,14 +69,15 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
onBackPressed: () => setState(() {
isAddingImage = false;
}),
onFileSubmit: (List<String> path) {
onFileSubmit: (_) {
context.read<ChangeCoverPopoverBloc>().add(
const ChangeCoverPopoverEvent
.fetchPickedImagePaths(),
.fetchPickedImagePaths(
selectLatestImage: true,
),
);
setState(() {
isAddingImage = false;
});
setState(() => isAddingImage = false);
},
)
: _buildCoverSelection(),
@@ -294,7 +273,8 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
},
);
}
return Container();
return const SizedBox.shrink();
},
);
}
@@ -314,6 +294,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
@visibleForTesting
class NewCustomCoverButton extends StatelessWidget {
final VoidCallback onPressed;
const NewCustomCoverButton({super.key, required this.onPressed});
@override
@@ -337,6 +318,85 @@ class NewCustomCoverButton extends StatelessWidget {
}
}
class ColorOption {
final String colorHex;
final String name;
const ColorOption({
required this.colorHex,
required this.name,
});
}
class CoverColorPicker extends StatefulWidget {
final String? selectedBackgroundColorHex;
final Color pickerBackgroundColor;
final Color pickerItemHoverColor;
final void Function(String color) onSubmittedBackgroundColorHex;
final List<ColorOption> backgroundColorOptions;
const CoverColorPicker({
super.key,
this.selectedBackgroundColorHex,
required this.pickerBackgroundColor,
required this.backgroundColorOptions,
required this.pickerItemHoverColor,
required this.onSubmittedBackgroundColorHex,
});
@override
State<CoverColorPicker> createState() => _CoverColorPickerState();
}
class _CoverColorPickerState extends State<CoverColorPicker> {
final scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Container(
height: 30,
alignment: Alignment.center,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
platform: TargetPlatform.windows,
),
child: SingleChildScrollView(
child: _buildColorItems(
widget.backgroundColorOptions,
widget.selectedBackgroundColorHex,
),
),
),
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: options
.map(
(e) => ColorItem(
option: e,
isChecked: e.colorHex == selectedColor,
hoverColor: widget.pickerItemHoverColor,
onTap: widget.onSubmittedBackgroundColorHex,
),
)
.toList(),
);
}
}
class DeleteImageAlertDialog extends StatelessWidget {
const DeleteImageAlertDialog({
Key? key,
@@ -458,55 +518,6 @@ class _ImageGridItemState extends State<ImageGridItem> {
}
}
class _CoverColorPickerState extends State<CoverColorPicker> {
final scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Container(
height: 30,
alignment: Alignment.center,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
platform: TargetPlatform.windows,
),
child: SingleChildScrollView(
child: _buildColorItems(
widget.backgroundColorOptions,
widget.selectedBackgroundColorHex,
),
),
),
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: options
.map(
(e) => ColorItem(
option: e,
isChecked: e.colorHex == selectedColor,
hoverColor: widget.pickerItemHoverColor,
onTap: widget.onSubmittedBackgroundColorHex,
),
)
.toList(),
);
}
}
@visibleForTesting
class ColorItem extends StatelessWidget {
final ColorOption option;
@@ -16,18 +16,28 @@ class ChangeCoverPopoverBloc
final Node node;
late final SharedPreferences _prefs;
final _initCompleter = Completer<void>();
ChangeCoverPopoverBloc({required this.editorState, required this.node})
: super(const ChangeCoverPopoverState.initial()) {
ChangeCoverPopoverBloc({
required this.editorState,
required this.node,
}) : super(const ChangeCoverPopoverState.initial()) {
SharedPreferences.getInstance().then((prefs) {
_prefs = prefs;
_initCompleter.complete();
});
on<ChangeCoverPopoverEvent>((event, emit) async {
await event.map(
fetchPickedImagePaths:
(FetchPickedImagePaths fetchPickedImagePaths) async {
final imageNames = await _getPreviouslyPickedImagePaths();
emit(ChangeCoverPopoverState.loaded(imageNames));
emit(
ChangeCoverPopoverState.loaded(
imageNames,
selectLatestImage: fetchPickedImagePaths.selectLatestImage,
),
);
},
deleteImage: (DeleteImage deleteImage) async {
final currentState = state;
@@ -100,8 +110,9 @@ class ChangeCoverPopoverBloc
@freezed
class ChangeCoverPopoverEvent with _$ChangeCoverPopoverEvent {
const factory ChangeCoverPopoverEvent.fetchPickedImagePaths() =
FetchPickedImagePaths;
const factory ChangeCoverPopoverEvent.fetchPickedImagePaths({
@Default(false) bool selectLatestImage,
}) = FetchPickedImagePaths;
const factory ChangeCoverPopoverEvent.deleteImage(String path) = DeleteImage;
const factory ChangeCoverPopoverEvent.clearAllImages() = ClearAllImages;
@@ -112,6 +123,7 @@ class ChangeCoverPopoverState with _$ChangeCoverPopoverState {
const factory ChangeCoverPopoverState.initial() = Initial;
const factory ChangeCoverPopoverState.loading() = Loading;
const factory ChangeCoverPopoverState.loaded(
List<String> imageNames,
) = Loaded;
List<String> imageNames, {
@Default(false) selectLatestImage,
}) = Loaded;
}
@@ -54,12 +54,9 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
MathEquationBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
});
@override
final BlockComponentConfiguration configuration;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
@@ -127,6 +127,7 @@ class _AutoCompletionBlockComponentState
_onExit();
_unsubscribeSelectionGesture();
controller.dispose();
textFieldFocusNode.dispose();
super.dispose();
}
@@ -32,12 +32,9 @@ Node outlineBlockNode() {
class OutlineBlockComponentBuilder extends BlockComponentBuilder {
OutlineBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
});
@override
final BlockComponentConfiguration configuration;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
@@ -14,6 +14,7 @@ export 'database/inline_database_menu_item.dart';
export 'database/referenced_database_menu_item.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'font/customize_font_toolbar_item.dart';
export 'header/cover_editor_bloc.dart';
export 'header/custom_cover_picker.dart';
export 'header/document_header_node_widget.dart';
@@ -55,13 +55,10 @@ SelectionMenuItem toggleListBlockItem = SelectionMenuItem.node(
class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
ToggleListBlockComponentBuilder({
this.configuration = const BlockComponentConfiguration(),
super.configuration,
this.padding = const EdgeInsets.all(0),
});
@override
final BlockComponentConfiguration configuration;
final EdgeInsets padding;
@override
@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_mat
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@@ -193,6 +194,15 @@ class EditorStyleCustomizer {
return textSpan;
}
// try to refresh font here.
if (attributes.fontFamily != null) {
try {
GoogleFonts.getFont(attributes.fontFamily!.parseFontFamilyName());
} catch (e) {
// ignore
}
}
// customize the inline mention block, like inline page
final mention = attributes[MentionBlockKeys.mention] as Map?;
if (mention != null) {
@@ -56,6 +56,7 @@ class InitAppWidgetTask extends LaunchTask {
Locale('ru', 'RU'),
Locale('sv'),
Locale('tr', 'TR'),
Locale('ur'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('fa'),
@@ -149,7 +150,6 @@ Future<AppTheme> appTheme(String themeName) async {
try {
return await AppTheme.fromName(themeName);
} catch (e) {
Log.error(e);
return AppTheme.fallback;
}
}
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -17,10 +16,7 @@ class UserBackendService {
static Future<Either<FlowyError, UserProfilePB>>
getCurrentUserProfile() async {
final result = await UserEventGetUserProfile().send().then((value) {
value.fold((l) => null, (r) => Log.info(r));
return value;
});
final result = await UserEventGetUserProfile().send();
return result.swap();
}
@@ -104,7 +104,7 @@ class SplashScreen extends StatelessWidget {
}
void _handleUnauthenticated(BuildContext context, Unauthenticated result) {
Log.debug(
Log.trace(
'_handleUnauthenticated -> Supabase is enabled: $isSupabaseEnabled',
);
// if the env is not configured, we will skip to the 'skip login screen'.
@@ -0,0 +1,7 @@
extension GoogleFontsParser on String {
String parseFontFamilyName() {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
}
@@ -300,10 +300,6 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
ThemeData get lightTheme => _getThemeData(Brightness.light);
ThemeData get darkTheme => _getThemeData(Brightness.dark);
// only support LTR layout in version 0.3.2, enable it in version 0.3.3
LayoutDirectionPB get layoutDirectionPB => LayoutDirectionPB.LTRLayout;
TextDirectionPB get textDirectionPB => TextDirectionPB.LTR;
ThemeData _getThemeData(Brightness brightness) {
// Poppins and SF Mono are not well supported in some languages, so use the
// built-in font for the following languages.
@@ -437,6 +433,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
toggleOffFill: theme.shader5,
progressBarBGColor: theme.progressBarBGColor,
toggleButtonBGColor: theme.toggleButtonBGColor,
calendarWeekendBGColor: theme.calendarWeekendBGColor,
gridRowCountColor: theme.gridRowCountColor,
code: _getFontStyle(
fontFamily: monospaceFontFamily,
fontColor: theme.shader3,
@@ -43,6 +43,15 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
);
});
},
removeUserIcon: () {
// Empty Icon URL = No icon
_userService.updateUserProfile(iconUrl: "").then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
},
updateUserOpenAIKey: (openAIKey) {
_userService.updateUserProfile(openAIKey: openAIKey).then((result) {
result.fold(
@@ -105,8 +114,9 @@ class SettingsUserEvent with _$SettingsUserEvent {
const factory SettingsUserEvent.initial() = _Initial;
const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName;
const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail;
const factory SettingsUserEvent.updateUserIcon(String iconUrl) =
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
_UpdateUserIcon;
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
_UpdateUserOpenaiKey;
const factory SettingsUserEvent.didReceiveUserProfile(
@@ -1,11 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:flowy_infra/size.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@@ -34,7 +32,10 @@ class SidebarUser extends StatelessWidget {
builder: (context, state) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildAvatar(context, state),
UserAvatar(
iconUrl: state.userProfile.iconUrl,
name: state.userProfile.name,
),
const HSpace(10),
Expanded(
child: _buildUserName(context, state),
@@ -46,50 +47,6 @@ class SidebarUser extends StatelessWidget {
);
}
Widget _buildAvatar(BuildContext context, MenuUserState state) {
String iconUrl = state.userProfile.iconUrl;
if (iconUrl.isEmpty) {
iconUrl = defaultUserAvatar;
final String name = _userName(state.userProfile);
final Color color = ColorGenerator().generateColorFromString(name);
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = name
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join('');
return Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: FlowyText.semibold(
nameInitials,
color: Colors.white,
fontSize: nameInitials.length == initialsCount ? 12 : 14,
),
);
}
return SizedBox.square(
dimension: 25,
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: FlowySvg(
FlowySvgData('emoji/$iconUrl'),
blendMode: null,
),
),
),
);
}
Widget _buildUserName(BuildContext context, MenuUserState state) {
final String name = _userName(state.userProfile);
return FlowyText.medium(
@@ -259,6 +259,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.secondary,
),
resetHoverOnRebuild: widget.showActions,
buildWhenOnHover: () => !widget.showActions,
builder: (_, onHover) => _buildViewItem(onHover),
isSelected: () =>
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -27,7 +28,7 @@ class BrightnessSetting extends StatelessWidget {
trailing: [
ThemeValueDropDown(
currentValue: _themeModeLabelText(currentThemeMode),
popupBuilder: (_) => Column(
popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_themeModeItemButton(context, ThemeMode.light),
@@ -52,7 +53,10 @@ class BrightnessSetting extends StatelessWidget {
],
);
Widget _themeModeItemButton(BuildContext context, ThemeMode themeMode) {
Widget _themeModeItemButton(
BuildContext context,
ThemeMode themeMode,
) {
return SizedBox(
height: 32,
child: FlowyButton(
@@ -66,6 +70,7 @@ class BrightnessSetting extends StatelessWidget {
if (currentThemeMode != themeMode) {
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
}
PopoverContainer.of(context).close();
},
),
);
@@ -30,8 +30,8 @@ class ColorSchemeSetting extends StatelessWidget {
label: LocaleKeys.settings_appearance_theme.tr(),
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
trailing: [
ColorSchemeUploadOverlayButton(bloc: bloc),
ColorSchemeUploadPopover(currentTheme: currentTheme, bloc: bloc),
ColorSchemeUploadOverlayButton(bloc: bloc),
],
);
}
@@ -156,6 +156,7 @@ class ColorSchemeUploadPopover extends StatelessWidget {
if (currentTheme != theme) {
context.read<AppearanceSettingsCubit>().setTheme(theme);
}
PopoverContainer.of(context).close();
},
),
),
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -24,8 +25,9 @@ class LayoutDirectionSetting extends StatelessWidget {
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
trailing: [
ThemeValueDropDown(
key: const ValueKey('layout_direction_option_button'),
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
popupBuilder: (_) => Column(
popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_layoutDirectionItemButton(context, LayoutDirection.ltrLayout),
@@ -54,6 +56,7 @@ class LayoutDirectionSetting extends StatelessWidget {
.read<AppearanceSettingsCubit>()
.setLayoutDirection(direction);
}
PopoverContainer.of(context).close();
},
),
);
@@ -86,7 +89,7 @@ class TextDirectionSetting extends StatelessWidget {
trailing: [
ThemeValueDropDown(
currentValue: _textDirectionLabelText(currentTextDirection),
popupBuilder: (_) => Column(
popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_textDirectionItemButton(context, null),
@@ -119,6 +122,7 @@ class TextDirectionSetting extends StatelessWidget {
.read<DocumentAppearanceCubit>()
.syncDefaultTextDirection(textDirection?.name);
}
PopoverContainer.of(context).close();
},
),
);
@@ -1,8 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -29,9 +31,6 @@ class ThemeFontFamilySetting extends StatefulWidget {
}
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
final ValueNotifier<String> query = ValueNotifier('');
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
@@ -44,61 +43,115 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
},
trailing: [
ThemeValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
currentValue: parseFontFamilyName(widget.currentFontFamily),
onClose: () {
query.value = '';
},
popupBuilder: (_) => CustomScrollView(
shrinkWrap: true,
slivers: [
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;
},
),
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 = GoogleFonts.asMap().keys.toList();
final ValueNotifier<String> query = ValueNotifier('');
@override
Widget build(BuildContext context) {
return ThemeValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: parseFontFamilyName(widget.currentFontFamily),
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,
GoogleFonts.getFont(displayed[index]),
),
itemCount: displayed.length,
itemExtent: 32,
);
},
),
],
),
),
],
),
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,
GoogleFonts.getFont(displayed[index]),
),
itemCount: displayed.length,
itemExtent: 32,
);
},
),
],
);
},
);
}
@@ -109,7 +162,10 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
Widget _fontFamilyItemButton(BuildContext context, TextStyle style) {
Widget _fontFamilyItemButton(
BuildContext context,
TextStyle style,
) {
final buttonFontFamily = parseFontFamilyName(style.fontFamily!);
return SizedBox(
key: UniqueKey(),
@@ -128,17 +184,54 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
)
: null,
onTap: () {
if (parseFontFamilyName(widget.currentFontFamily) !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(parseFontFamilyName(style.fontFamily!));
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(parseFontFamilyName(style.fontFamily!));
if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(style.fontFamily!);
} else {
final fontFamily = style.fontFamily!.parseFontFamilyName();
if (parseFontFamilyName(widget.currentFontFamily) !=
buttonFontFamily) {
context.read<AppearanceSettingsCubit>().setFontFamily(fontFamily);
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(fontFamily);
}
}
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(),
onPressed: onPressed,
),
);
}
@override
double get maxExtent => 35;
@override
double get minExtent => 35;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
@@ -31,6 +31,7 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
children: [
FlowyText.medium(
label,
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
if (hint != null)
@@ -71,12 +72,18 @@ class ThemeValueDropDown extends StatefulWidget {
required this.popupBuilder,
this.popoverKey,
this.onClose,
this.child,
this.popoverController,
this.offset,
});
final String currentValue;
final Key? popoverKey;
final Widget Function(BuildContext) popupBuilder;
final void Function()? onClose;
final Widget? child;
final PopoverController? popoverController;
final Offset? offset;
@override
State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
@@ -87,19 +94,23 @@ class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
Widget build(BuildContext context) {
return AppFlowyPopover(
key: widget.popoverKey,
direction: PopoverDirection.bottomWithRightAligned,
controller: widget.popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: widget.popupBuilder,
constraints: const BoxConstraints(
minWidth: 80,
maxWidth: 160,
maxHeight: 400,
),
offset: widget.offset,
onClose: widget.onClose,
child: FlowyTextButton(
widget.currentValue,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
),
child: widget.child ??
FlowyTextButton(
widget.currentValue,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
onPressed: () {},
),
);
}
}
@@ -19,23 +19,25 @@ class SettingsAppearanceView extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BrightnessSetting(
currentThemeMode: state.themeMode,
),
ColorSchemeSetting(
currentTheme: state.appTheme.themeName,
bloc: context.read<DynamicPluginBloc>(),
),
BrightnessSetting(
currentThemeMode: state.themeMode,
),
const Divider(),
ThemeFontFamilySetting(
currentFontFamily: state.font,
),
// TODO: enablt them in version 0.3.3
// LayoutDirectionSetting(
// currentLayoutDirection: state.layoutDirection,
// ),
// TextDirectionSetting(
// currentTextDirection: state.textDirection,
// ),
const Divider(),
LayoutDirectionSetting(
currentLayoutDirection: state.layoutDirection,
),
TextDirectionSetting(
currentTextDirection: state.textDirection,
),
const Divider(),
CreateFileSettings(),
],
);
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more