Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36a846425 | ||
|
|
a02ec98e9c | ||
|
|
32c06cf1bf | ||
|
|
cf1096b516 | ||
|
|
938953d7b3 | ||
|
|
949dab9afe | ||
|
|
0996f5fd24 | ||
|
|
6e887f1f12 | ||
|
|
ab9338eb89 | ||
|
|
fd4088aed0 | ||
|
|
375ff5ed6d | ||
|
|
847290d9a4 | ||
|
|
56687581f2 | ||
|
|
3be39f5e7b | ||
|
|
1997231adf | ||
|
|
89face4f02 | ||
|
|
37ddce3a29 | ||
|
|
593df96b10 | ||
|
|
1b966171c4 | ||
|
|
047f1a0b39 | ||
|
|
f2f3506b29 | ||
|
|
0ccced4b54 | ||
|
|
f762a564b5 | ||
|
|
57b78ee3c6 | ||
|
|
5dfc470873 | ||
|
|
d5f8ce5435 | ||
|
|
3c65a96b04 | ||
|
|
6ba7fc0317 | ||
|
|
37a5054ebd | ||
|
|
048434024b | ||
|
|
2c757e9b6c | ||
|
|
9c59e1487e | ||
|
|
4b9b723521 | ||
|
|
a389836897 | ||
|
|
eec7723ce0 | ||
|
|
2f938a331f | ||
|
|
847d7cb35a | ||
|
|
094f8fc307 | ||
|
|
e3f11ea9c0 | ||
|
|
124d435f09 | ||
|
|
b700f95c7f |
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+12
-1
@@ -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();
|
||||
}
|
||||
|
||||
+9
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+82
@@ -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());
|
||||
}
|
||||
+4
-1
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+102
-56
@@ -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,
|
||||
|
||||
+106
-36
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+280
@@ -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;
|
||||
}
|
||||
}
|
||||
+196
-48
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
-159
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-11
@@ -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()} :';
|
||||
}
|
||||
+6
-1
@@ -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
|
||||
|
||||
+16
@@ -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 {
|
||||
|
||||
+11
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -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;
|
||||
}
|
||||
|
||||
-77
@@ -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";
|
||||
}
|
||||
-90
@@ -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;
|
||||
}
|
||||
-68
@@ -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() ?? "",
|
||||
);
|
||||
}
|
||||
-78
@@ -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 ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
-79
@@ -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,
|
||||
);
|
||||
}
|
||||
-80
@@ -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 ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
-80
@@ -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 ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
-68
@@ -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();
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -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()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+5
-5
@@ -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();
|
||||
|
||||
+7
-7
@@ -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) {
|
||||
|
||||
+12
-11
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
+8
-7
@@ -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;
|
||||
},
|
||||
|
||||
+12
-12
@@ -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,
|
||||
|
||||
+7
-7
@@ -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) {
|
||||
|
||||
+7
-7
@@ -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) {
|
||||
|
||||
+4
-1
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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';
|
||||
+5
-8
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -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";
|
||||
}
|
||||
+36
-13
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+87
-120
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+138
-49
@@ -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);
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+12
-1
@@ -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 {
|
||||
|
||||
+189
-45
@@ -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
|
||||
|
||||
+46
-31
@@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
+6
@@ -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(
|
||||
|
||||
+9
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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(
|
||||
|
||||
+11
-8
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+1
-4
@@ -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
|
||||
|
||||
+1
-4
@@ -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
|
||||
|
||||
+10
-5
@@ -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) {
|
||||
|
||||
+20
-20
@@ -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;
|
||||
|
||||
+42
-31
@@ -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,
|
||||
),);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+50
-24
@@ -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
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
+48
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
+97
-86
@@ -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;
|
||||
|
||||
+19
-7
@@ -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;
|
||||
}
|
||||
+1
-4
@@ -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;
|
||||
|
||||
+1
@@ -127,6 +127,7 @@ class _AutoCompletionBlockComponentState
|
||||
_onExit();
|
||||
_unsubscribeSelectionGesture();
|
||||
controller.dispose();
|
||||
textFieldFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
+1
-4
@@ -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;
|
||||
|
||||
+1
@@ -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';
|
||||
|
||||
+1
-4
@@ -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(
|
||||
|
||||
+5
-48
@@ -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: () =>
|
||||
|
||||
+7
-2
@@ -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();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+2
-1
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+6
-2
@@ -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();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+158
-65
@@ -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;
|
||||
}
|
||||
+17
-6
@@ -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: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -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
Reference in new issue
Block a user