Merge branch 'upstream-main' into feat/tauri-kanban

This commit is contained in:
ascarbek 2023-03-29 13:18:07 +06:00
commit b03b2705f0
231 changed files with 5693 additions and 2782 deletions

View File

@ -1,5 +1,17 @@
# Release Notes
## Version 0.1.1 - 03/21/2023
### New features
- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy.
- Support adding a cover image to your page, making your pages beautiful.
- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide.
### Bug Fixes
- Fix some bugs
## Version 0.1.0 - 02/09/2023
### New features

View File

@ -44,16 +44,16 @@ You are in charge of your data and customizations.
<p align="center"><img src="https://github.com/AppFlowy-IO/appflowy/blob/main/doc/imgs/howtostar.gif" alt="AppFlowy Github - how to star the repo" width="100%" /></p>
## Getting Started with development
Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions
## Roadmap
- [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
## **Releases**
@ -61,37 +61,37 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
## Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
## Join the community to build AppFlowy together
## Join the community to build AppFlowy together!
<a href="https://github.com/AppFlowy-IO/AppFlowy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AppFlowy-IO/AppFlowy" />
</a>
## Why Are We Building This?
Notion has been our favorite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
The limitations we encountered using these tools rooted in our past work experience with collaborative productivity tools lead to our firm belief that there is, and will be a glass ceiling on what's possible in the future for tools like Notion. This emanates from these tools probable struggles to scale horizontally at some point. It implies that they will likely be forced to prioritize for a proportion of customers whose needs can be quite different from the rest. While decision-makers want a workplace OS, the truth is that it is not very possible to come up with a one-size fits all solution in such a fragmented market.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
- To individuals, we would like to offer Notion's functionality along with data security and cross-platform native experience.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks, that is, collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
We decided to achieve this mission by upholding the three most fundamental values:
- Data privacy first
- Reliable native experience
- Community-driven extensibility
* Data privacy first
* Reliable native experience
* Community-driven extensibility
To be honest, we do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools, while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
## License
@ -101,6 +101,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF
Special thanks to these amazing projects which help power AppFlowy.IO:
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
* [flutter-quill](https://github.com/singerdmx/flutter-quill)
* [cargo-make](https://github.com/sagiegurari/cargo-make)
* [contrib.rocks](https://contrib.rocks)

View File

@ -12,8 +12,8 @@
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
// "RUST_LOG": "trace",
"RUST_LOG": "debug"
"RUST_LOG": "trace",
// "RUST_LOG": "debug"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},

View File

@ -23,7 +23,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.1.0"
CURRENT_APP_VERSION = "0.1.1"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

View File

@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: fa5883b78e566877613ad1ccb48dd92075cb5c23
channel: dev
revision: 135454af32477f815a7525073027a3ff9eff1bfd
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
- platform: windows
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -45,7 +45,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.appflowy_flutter"
applicationId "io.appflowy.appflowy"
minSdkVersion 19
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.appflowy_flutter">
package="io.appflowy.appflowy">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.appflowy_flutter">
package="io.appflowy.appflowy">
<application
android:label="appflowy_flutter"
android:icon="@mipmap/ic_launcher"

View File

@ -1,4 +1,4 @@
package com.example.appflowy_flutter
package io.appflowy.appflowy
import io.flutter.embedding.android.FlutterActivity

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.appflowy_flutter">
package="io.appflowy.appflowy">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->

View File

@ -138,7 +138,8 @@
"keep": "Keep",
"tryAgain": "Try again",
"discard": "Discard",
"replace": "Replace"
"replace": "Replace",
"insertBelow": "Insert Below"
},
"label": {
"welcome": "Welcome!",
@ -197,7 +198,8 @@
"browser": "Browse",
"create": "Create",
"folderPath": "Path to store your folder",
"locationCannotBeEmpty": "Path cannot be empty"
"locationCannotBeEmpty": "Path cannot be empty",
"pathCopiedSnackbar": "File storage path copied to clipboard!"
},
"user": {
"name": "Name",
@ -216,7 +218,8 @@
"addFilter": "Add Filter",
"deleteFilter": "Delete filter",
"filterBy": "Filter by...",
"typeAValue": "Type a value..."
"typeAValue": "Type a value...",
"layout": "Layout"
},
"textFilter": {
"contains": "Contains",
@ -333,29 +336,32 @@
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "Select a Board to link to"
"selectABoardToLinkTo": "Select a Board to link to",
"createANewBoard": "Create a new Board"
},
"grid": {
"selectAGridToLinkTo": "Select a Grid to link to"
"selectAGridToLinkTo": "Select a Grid to link to",
"createANewGrid": "Create a new Grid"
}
},
"plugins": {
"referencedBoard": "Referenced Board",
"referencedGrid": "Referenced Grid",
"autoCompletionMenuItemName": "Auto Completion",
"autoGeneratorMenuItemName": "Auto Generator",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
"autoGeneratorHintText": "Ask OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
"smartEdit": "Smart Edit",
"smartEditTitleName": "OpenAI: Smart Edit",
"smartEdit": "AI Assistants",
"openAI": "OpenAI",
"smartEditFixSpelling": "Fix spelling",
"warning": "⚠️ AI responses can be inaccurate or misleading.",
"smartEditSummarize": "Summarize",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"discardResponse": "Do you want to discard the AI responses?",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
@ -377,6 +383,7 @@
"imageSavingFailed": "Image Saving Failed",
"addIcon": "Add Icon"
}
}
},
"board": {
@ -393,6 +400,12 @@
"jumpToday": "Jump to Today",
"previousMonth": "Previous Month",
"nextMonth": "Next Month"
},
"settings": {
"showWeekNumbers": "Show week numbers",
"showWeekends": "Show weekends",
"firstDayOfWeek": "First day of week",
"layoutDateField": "Layout calendar by"
}
}
}
}

View File

@ -349,7 +349,6 @@
"autoGeneratorGenerate": "Gerar",
"autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...",
"autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI",
"smartEditTitleName": "IA: edição inteligente",
"smartEditFixSpelling": "Corrigir ortografia",
"smartEditSummarize": "Resumir",
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",

View File

@ -1,5 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
import 'package:appflowy/user/presentation/folder/folder_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -86,7 +91,7 @@ void main() {
await tester.tapGoButton();
await tester.expectToSeeWelcomePage();
// swith to user B
// switch to user B
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
@ -120,7 +125,7 @@ void main() {
expect(find.textContaining(userA), findsOneWidget);
}
// swith to the userB again
// switch to the userB again
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.files);
@ -157,5 +162,135 @@ void main() {
await TestFolder.currentLocation(),
);
});
testWidgets('/board shortcut creates a new board', (tester) async {
const folderName = 'appflowy';
await TestFolder.cleanTestLocation(folderName);
await TestFolder.setTestLocation(folderName);
await tester.initializeAppFlowy();
// tap open button
await mockGetDirectoryPath(folderName);
await tester.tapOpenFolderButton();
await tester.wait(1000);
await tester.expectToSeeWelcomePage();
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Necessary for being able to enterText when not in debug mode
binding.testTextInput.register();
// Needs tab to obtain focus for the app flowy editor.
// by default the tap appears at the center of the widget.
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
// tester.sendText() cannot be used since the editor
// does not contain any EditableText widgets.
// to interact with the app during an integration test,
// simulate physical keyboard events.
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.slash);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyB);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyO);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
// Checks whether the options in the selection menu
// for /board exist.
expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
// Finalizes the slash command that creates the board.
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
// Checks whether new board is referenced and properly on the page.
expect(find.byType(BuiltInPageWidget), findsOneWidget);
// Checks whether the new board is in the side bar.
final sidebarLabel = LocaleKeys.newPageText.tr();
expect(find.text(sidebarLabel), findsOneWidget);
});
testWidgets('/grid shortcut creates a new grid', (tester) async {
const folderName = 'appflowy';
await TestFolder.cleanTestLocation(folderName);
await TestFolder.setTestLocation(folderName);
await tester.initializeAppFlowy();
// tap open button
await mockGetDirectoryPath(folderName);
await tester.tapOpenFolderButton();
await tester.wait(1000);
await tester.expectToSeeWelcomePage();
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Necessary for being able to enterText when not in debug mode
binding.testTextInput.register();
// Needs tab to obtain focus for the app flowy editor.
// by default the tap appears at the center of the widget.
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
// tester.sendText() cannot be used since the editor
// does not contain any EditableText widgets.
// to interact with the app during an integration test,
// simulate physical keyboard events.
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.slash);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyG);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyI);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
// Checks whether the options in the selection menu
// for /grid exist.
expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
// Finalizes the slash command that creates the board.
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
// Checks whether new board is referenced and properly on the page.
expect(find.byType(BuiltInPageWidget), findsOneWidget);
// Checks whether the new board is in the side bar.
final sidebarLabel = LocaleKeys.newPageText.tr();
expect(find.text(sidebarLabel), findsOneWidget);
});
});
}

View File

@ -39,7 +39,7 @@ extension AppFlowySettings on WidgetTester {
return;
}
/// Open the page taht insides the settings page
/// Open the page that insides the settings page
Future<void> openSettingsPage(SettingsPage page) async {
final button = find.text(page.name, findRichText: true);
expect(button, findsOneWidget);
@ -49,25 +49,25 @@ extension AppFlowySettings on WidgetTester {
/// Restore the AppFlowy data storage location
Future<void> restoreLocation() async {
final buton =
final button =
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
expect(buton, findsOneWidget);
await tapButton(buton);
expect(button, findsOneWidget);
await tapButton(button);
return;
}
Future<void> tapOpenFolderButton() async {
final buton = find.text(LocaleKeys.settings_files_open.tr());
expect(buton, findsOneWidget);
await tapButton(buton);
final button = find.text(LocaleKeys.settings_files_open.tr());
expect(button, findsOneWidget);
await tapButton(button);
return;
}
Future<void> tapCustomLocationButton() async {
final buton =
final button =
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
expect(buton, findsOneWidget);
await tapButton(buton);
expect(button, findsOneWidget);
await tapButton(button);
return;
}

View File

@ -359,7 +359,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -483,7 +483,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -502,7 +502,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
@ -18,11 +19,6 @@ import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
import 'group/group_listener.dart';
typedef OnRowsChanged = void Function(
List<RowInfo> rowInfos,
RowsChangedReason,
);
typedef OnGroupByField = void Function(List<GroupPB>);
typedef OnUpdateGroup = void Function(List<GroupPB>);
typedef OnDeleteGroup = void Function(List<String>);
@ -52,16 +48,29 @@ class LayoutCallbacks {
});
}
class CalendarLayoutCallbacks {
final void Function(LayoutSettingPB) onCalendarLayoutChanged;
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
}
class DatabaseCallbacks {
OnDatabaseChanged? onDatabaseChanged;
OnRowsChanged? onRowsChanged;
OnFieldsChanged? onFieldsChanged;
OnFiltersChanged? onFiltersChanged;
OnRowsChanged? onRowsChanged;
OnRowsDeleted? onRowsDeleted;
OnRowsUpdated? onRowsUpdated;
OnRowsCreated? onRowsCreated;
DatabaseCallbacks({
this.onDatabaseChanged,
this.onRowsChanged,
this.onFieldsChanged,
this.onFiltersChanged,
this.onRowsUpdated,
this.onRowsDeleted,
this.onRowsCreated,
});
}
@ -76,21 +85,23 @@ class DatabaseController {
DatabaseCallbacks? _databaseCallbacks;
GroupCallbacks? _groupCallbacks;
LayoutCallbacks? _layoutCallbacks;
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
// Getters
List<RowInfo> get rowInfos => _viewCache.rowInfos;
RowCache get rowCache => _viewCache.rowCache;
// Listener
final DatabaseGroupListener groupListener;
final DatabaseLayoutListener layoutListener;
final DatabaseCalendarLayoutListener calendarLayoutListener;
DatabaseController({required ViewPB view, required this.layoutType})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
groupListener = DatabaseGroupListener(view.id),
layoutListener = DatabaseLayoutListener(view.id) {
layoutListener = DatabaseLayoutListener(view.id),
calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
_viewCache = DatabaseViewCache(
viewId: viewId,
fieldController: fieldController,
@ -99,16 +110,21 @@ class DatabaseController {
_listenOnFieldsChanged();
_listenOnGroupChanged();
_listenOnLayoutChanged();
if (layoutType == LayoutTypePB.Calendar) {
_listenOnCalendarLayoutChanged();
}
}
void addListener({
DatabaseCallbacks? onDatabaseChanged,
LayoutCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
CalendarLayoutCallbacks? onCalendarLayoutChanged,
}) {
_layoutCallbacks = onLayoutChanged;
_databaseCallbacks = onDatabaseChanged;
_groupCallbacks = onGroupChanged;
_calendarLayoutCallbacks = onCalendarLayoutChanged;
}
Future<Either<Unit, FlowyError>> open() async {
@ -218,9 +234,17 @@ class DatabaseController {
}
void _listenOnRowsChanged() {
_viewCache.addListener(onRowsChanged: (reason) {
_databaseCallbacks?.onRowsChanged?.call(rowInfos, reason);
final callbacks =
DatabaseViewCallbacks(onRowsChanged: (rows, rowByRowId, reason) {
_databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason);
}, onRowsDeleted: (ids) {
_databaseCallbacks?.onRowsDeleted?.call(ids);
}, onRowsUpdated: (ids) {
_databaseCallbacks?.onRowsUpdated?.call(ids);
}, onRowsCreated: (ids) {
_databaseCallbacks?.onRowsCreated?.call(ids);
});
_viewCache.addListener(callbacks);
}
void _listenOnFieldsChanged() {
@ -266,6 +290,14 @@ class DatabaseController {
}, (r) => Log.error(r));
});
}
void _listenOnCalendarLayoutChanged() {
calendarLayoutListener.start(onCalendarLayoutChanged: (result) {
result.fold((l) {
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
}, (r) => Log.error(r));
});
}
}
class RowDataBuilder {

View File

@ -10,9 +10,14 @@ import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<String> ids);
typedef OnRowsUpdated = void Function(List<String> ids);
typedef OnRowsDeleted = void Function(List<String> ids);
typedef OnRowsChanged = void Function(
List<RowInfo>,
RowsChangedReason,
UnmodifiableListView<RowInfo> rows,
UnmodifiableMapView<String, RowInfo> rowByRowId,
RowsChangedReason reason,
);
typedef OnError = void Function(FlowyError);

View File

@ -162,7 +162,7 @@ class FieldController {
//Listen on setting changes
_listenOnSettingChanges();
//Listen on the fitler changes
//Listen on the filter changes
_listenOnFilterChanges();
//Listen on the sort changes
@ -177,7 +177,7 @@ class FieldController {
}
void _listenOnFilterChanges() {
//Listen on the fitler changes
//Listen on the filter changes
deleteFilterFromChangeset(
List<FilterInfo> filters,
@ -230,7 +230,7 @@ class FieldController {
.removeWhere((key, value) => value.id == updatedFilter.filterId);
}
// Insert the filter if there is a fitler and its field info is
// Insert the filter if there is a filter and its field info is
// not null
if (updatedFilter.hasFilter()) {
final fieldInfo = _findFieldInfo(

View File

@ -0,0 +1,49 @@
import 'dart:typed_data';
import 'package:appflowy/core/grid_notification.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
import 'package:dartz/dartz.dart';
typedef NewLayoutFieldValue = Either<LayoutSettingPB, FlowyError>;
class DatabaseCalendarLayoutListener {
final String viewId;
PublishNotifier<NewLayoutFieldValue>? _newLayoutFieldNotifier =
PublishNotifier();
DatabaseNotificationListener? _listener;
DatabaseCalendarLayoutListener(this.viewId);
void start(
{required void Function(NewLayoutFieldValue) onCalendarLayoutChanged}) {
_newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged);
_listener = DatabaseNotificationListener(
objectId: viewId,
handler: _handler,
);
}
void _handler(
DatabaseNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidSetNewLayoutField:
result.fold(
(payload) => _newLayoutFieldNotifier?.value =
left(LayoutSettingPB.fromBuffer(payload)),
(error) => _newLayoutFieldNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_newLayoutFieldNotifier?.dispose();
_newLayoutFieldNotifier = null;
}
}

View File

@ -37,11 +37,15 @@ class RowCache {
final RowCacheDelegate _delegate;
final RowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<RowInfo> get visibleRows {
UnmodifiableListView<RowInfo> get rowInfos {
var visibleRows = [..._rowList.rows];
return UnmodifiableListView(visibleRows);
}
UnmodifiableMapView<String, RowInfo> get rowByRowId {
return UnmodifiableMapView(_rowList.rowInfoByRowId);
}
CellCache get cellCache => _cellCache;
RowCache({
@ -61,6 +65,10 @@ class RowCache {
});
}
RowInfo? getRow(String rowId) {
return _rowList.get(rowId);
}
void setInitialRows(List<RowPB> rows) {
for (final row in rows) {
final rowInfo = buildGridRow(row);

View File

@ -9,14 +9,14 @@ class RowList {
List<RowInfo> get rows => List.from(_rowInfos);
/// Use Map for faster access the raw row data.
final HashMap<String, RowInfo> _rowInfoByRowId = HashMap();
final HashMap<String, RowInfo> rowInfoByRowId = HashMap();
RowInfo? get(String rowId) {
return _rowInfoByRowId[rowId];
return rowInfoByRowId[rowId];
}
int? indexOfRow(String rowId) {
final rowInfo = _rowInfoByRowId[rowId];
final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
return _rowInfos.indexOf(rowInfo);
}
@ -33,7 +33,7 @@ class RowList {
} else {
_rowInfos.add(rowInfo);
}
_rowInfoByRowId[rowId] = rowInfo;
rowInfoByRowId[rowId] = rowInfo;
}
InsertedIndex? insert(int index, RowInfo rowInfo) {
@ -47,21 +47,21 @@ class RowList {
if (oldRowInfo != null) {
_rowInfos.insert(insertedIndex, rowInfo);
_rowInfos.remove(oldRowInfo);
_rowInfoByRowId[rowId] = rowInfo;
rowInfoByRowId[rowId] = rowInfo;
return null;
} else {
_rowInfos.insert(insertedIndex, rowInfo);
_rowInfoByRowId[rowId] = rowInfo;
rowInfoByRowId[rowId] = rowInfo;
return InsertedIndex(index: insertedIndex, rowId: rowId);
}
}
DeletedIndex? remove(String rowId) {
final rowInfo = _rowInfoByRowId[rowId];
final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
final index = _rowInfos.indexOf(rowInfo);
if (index != -1) {
_rowInfoByRowId.remove(rowInfo.rowPB.id);
rowInfoByRowId.remove(rowInfo.rowPB.id);
_rowInfos.remove(rowInfo);
}
return DeletedIndex(index: index, rowInfo: rowInfo);
@ -105,7 +105,7 @@ class RowList {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
_rowInfoByRowId.remove(rowInfo.rowPB.id);
rowInfoByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
}
});
@ -136,7 +136,7 @@ class RowList {
_rowInfos.clear();
for (final rowId in rowIds) {
final rowInfo = _rowInfoByRowId[rowId];
final rowInfo = rowInfoByRowId[rowId];
if (rowInfo != null) {
_rowInfos.add(rowInfo);
}
@ -155,6 +155,6 @@ class RowList {
}
bool contains(String rowId) {
return _rowInfoByRowId[rowId] != null;
return rowInfoByRowId[rowId] != null;
}
}

View File

@ -27,7 +27,7 @@ class SettingController {
);
});
// Listen on the seting changes
// Listen on the setting changes
_listener.start(onSettingUpdated: (result) {
result.fold(
(newSetting) => updateSetting(newSetting),
@ -36,7 +36,7 @@ class SettingController {
});
}
void startListeing({
void startListening({
required OnSettingUpdated onSettingUpdated,
required OnError onError,
}) {

View File

@ -1,22 +1,50 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy_backend/log.dart';
import '../defines.dart';
import '../field/field_controller.dart';
import '../row/row_cache.dart';
import 'view_listener.dart';
class DatabaseViewCallbacks {
/// Will get called when number of rows were changed that includes
/// update/delete/insert rows. The [onRowsChanged] will return all
/// the rows of the current database
final OnRowsChanged? onRowsChanged;
// Will get called when creating new rows
final OnRowsCreated? onRowsCreated;
/// Will get called when number of rows were updated
final OnRowsUpdated? onRowsUpdated;
/// Will get called when number of rows were deleted
final OnRowsDeleted? onRowsDeleted;
const DatabaseViewCallbacks({
this.onRowsChanged,
this.onRowsCreated,
this.onRowsUpdated,
this.onRowsDeleted,
});
}
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
class DatabaseViewCache {
final String viewId;
late RowCache _rowCache;
final DatabaseViewListener _gridViewListener;
final DatabaseViewListener _databaseViewListener;
DatabaseViewCallbacks? _callbacks;
List<RowInfo> get rowInfos => _rowCache.visibleRows;
UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
RowCache get rowCache => _rowCache;
RowInfo? getRow(String rowId) => _rowCache.getRow(rowId);
DatabaseViewCache({
required this.viewId,
required FieldController fieldController,
}) : _gridViewListener = DatabaseViewListener(viewId: viewId) {
}) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {
final delegate = RowDelegatesImpl(fieldController);
_rowCache = RowCache(
viewId: viewId,
@ -24,10 +52,28 @@ class DatabaseViewCache {
cacheDelegate: delegate,
);
_gridViewListener.start(
_databaseViewListener.start(
onRowsChanged: (result) {
result.fold(
(changeset) => _rowCache.applyRowsChanged(changeset),
(changeset) {
// Update the cache
_rowCache.applyRowsChanged(changeset);
if (changeset.deletedRows.isNotEmpty) {
_callbacks?.onRowsDeleted?.call(changeset.deletedRows);
}
if (changeset.updatedRows.isNotEmpty) {
_callbacks?.onRowsUpdated
?.call(changeset.updatedRows.map((e) => e.row.id).toList());
}
if (changeset.insertedRows.isNotEmpty) {
_callbacks?.onRowsCreated?.call(changeset.insertedRows
.map((insertedRow) => insertedRow.row.id)
.toList());
}
},
(err) => Log.error(err),
);
},
@ -50,23 +96,22 @@ class DatabaseViewCache {
);
},
);
_rowCache.onRowsChanged(
(reason) => _callbacks?.onRowsChanged?.call(
rowInfos,
_rowCache.rowByRowId,
reason,
),
);
}
Future<void> dispose() async {
await _gridViewListener.stop();
await _databaseViewListener.stop();
await _rowCache.dispose();
}
void addListener({
required void Function(RowsChangedReason) onRowsChanged,
bool Function()? listenWhen,
}) {
_rowCache.onRowsChanged((reason) {
if (listenWhen != null && listenWhen() == false) {
return;
}
onRowsChanged(reason);
});
void addListener(DatabaseViewCallbacks callbacks) {
_callbacks = callbacks;
}
}

View File

@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart';
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
final DatabaseController _databaseController;
Map<String, FieldInfo> fieldInfoByFieldId = {};
// Getters
String get viewId => _databaseController.viewId;
FieldController get fieldController => _databaseController.fieldController;
CellCache get cellCache => _databaseController.rowCache.cellCache;
RowCache get rowCache => _databaseController.rowCache;
@ -28,7 +30,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
view: view,
layoutType: LayoutTypePB.Calendar,
),
super(CalendarState.initial(view.id)) {
super(CalendarState.initial()) {
on<CalendarEvent>(
(event, emit) async {
await event.when(
@ -44,16 +46,49 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
emit(state.copyWith(database: Some(database)));
},
didLoadAllEvents: (events) {
emit(state.copyWith(events: events));
emit(state.copyWith(initialEvents: events, allEvents: events));
},
didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) {
_loadAllEvents();
emit(state.copyWith(settings: Some(layoutSettings)));
},
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
didReceiveEvent: (CalendarEventData<CalendarCardData> newEvent) {
emit(state.copyWith(events: [...state.events, newEvent]));
updateCalendarLayoutSetting:
(CalendarLayoutSettingsPB layoutSetting) async {
await _updateCalendarLayoutSetting(layoutSetting);
},
didUpdateFieldInfos: (Map<String, FieldInfo> fieldInfoByFieldId) {
emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
var allEvents = [...state.allEvents];
final index = allEvents.indexWhere(
(element) => element.event!.cellId == eventData.event!.cellId,
);
if (index != -1) {
allEvents[index] = eventData;
}
emit(state.copyWith(
allEvents: allEvents,
updateEvent: eventData,
));
},
didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(state.copyWith(
allEvents: [...state.allEvents, event],
newEvent: event,
));
},
didDeleteEvents: (List<String> deletedRowIds) {
var events = [...state.allEvents];
events.retainWhere(
(element) => !deletedRowIds.contains(element.event!.cellId.rowId),
);
emit(
state.copyWith(
allEvents: events,
deleteEventIds: deletedRowIds,
),
);
},
);
},
@ -97,7 +132,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
Future<void> _createEvent(DateTime date, String title) async {
state.settings.fold(
return state.settings.fold(
() => null,
(settings) async {
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
@ -110,8 +145,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
},
);
result.fold(
(newRow) => _loadEvent(newRow.id),
return result.fold(
(newRow) {},
(err) => Log.error(err),
);
}
@ -119,17 +154,23 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
}
Future<void> _loadEvent(String rowId) async {
Future<void> _updateCalendarLayoutSetting(
CalendarLayoutSettingsPB layoutSetting) async {
return _databaseController.updateCalenderLayoutSetting(layoutSetting);
}
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(String rowId) async {
final payload = RowIdPB(viewId: viewId, rowId: rowId);
DatabaseEventGetCalendarEvent(payload).send().then((result) {
result.fold(
return DatabaseEventGetCalendarEvent(payload).send().then((result) {
return result.fold(
(eventPB) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
add(CalendarEvent.didReceiveEvent(calendarEvent));
}
return calendarEvent;
},
(r) {
Log.error(r);
return null;
},
(r) => Log.error(r),
);
});
}
@ -140,7 +181,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
result.fold(
(events) {
if (!isClosed) {
final calendarEvents = <CalendarEventData<CalendarCardData>>[];
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
for (final eventPB in events.items) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
@ -156,9 +197,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
});
}
CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
CalendarEventPB eventPB) {
final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
if (fieldInfo != null) {
final cellId = CellIdentifier(
viewId: viewId,
@ -166,7 +207,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
fieldInfo: fieldInfo,
);
final eventData = CalendarCardData(
final eventData = CalendarDayEvent(
event: eventPB,
cellId: cellId,
);
@ -192,10 +233,31 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
},
onFieldsChanged: (fieldInfos) {
if (isClosed) return;
final fieldInfoByFieldId = {
fieldInfoByFieldId = {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
};
add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId));
},
onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
onRowsCreated: ((ids) async {
for (final id in ids) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(CalendarEvent.didReceiveNewEvent(event));
}
}
}),
onRowsDeleted: (ids) {
if (isClosed) return;
add(CalendarEvent.didDeleteEvents(ids));
},
onRowsUpdated: (ids) async {
if (isClosed) return;
for (final id in ids) {
final event = await _loadEvent(id);
if (event != null) {
add(CalendarEvent.didUpdateEvent(event));
}
}
},
);
@ -204,9 +266,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
onLoadLayout: _didReceiveLayoutSetting,
);
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
onCalendarLayoutChanged: _didReceiveNewLayoutField);
_databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
);
}
@ -216,44 +282,75 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
}
}
void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) {
if (layoutSetting.hasCalendar()) {
if (isClosed) return;
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
}
}
}
typedef Events = List<CalendarEventData<CalendarCardData>>;
typedef Events = List<CalendarEventData<CalendarDayEvent>>;
@freezed
class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.initial() = _InitialCalendar;
// Called after loading the calendar layout setting from the backend
const factory CalendarEvent.didReceiveCalendarSettings(
CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
// Called after loading all the current evnets
const factory CalendarEvent.didLoadAllEvents(Events events) =
_ReceiveCalendarEvents;
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarCardData> event) = _ReceiveEvent;
const factory CalendarEvent.didUpdateFieldInfos(
Map<String, FieldInfo> fieldInfoByFieldId) = _DidUpdateFieldInfos;
// Called when specific event was updated
const factory CalendarEvent.didUpdateEvent(
CalendarEventData<CalendarDayEvent> event) = _DidUpdateEvent;
// Called after creating a new event
const factory CalendarEvent.didReceiveNewEvent(
CalendarEventData<CalendarDayEvent> event) = _DidReceiveNewEvent;
// Called when deleting events
const factory CalendarEvent.didDeleteEvents(List<String> rowIds) =
_DidDeleteEvents;
// Called when creating a new event
const factory CalendarEvent.createEvent(DateTime date, String title) =
_CreateEvent;
// Called when updating the calendar's layout settings
const factory CalendarEvent.updateCalendarLayoutSetting(
CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting;
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
_ReceiveDatabaseUpdate;
const factory CalendarEvent.didReceiveNewLayoutField(
CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField;
}
@freezed
class CalendarState with _$CalendarState {
const factory CalendarState({
required String databaseId,
required Option<DatabasePB> database,
required Events events,
required Map<String, FieldInfo> fieldInfoByFieldId,
required Events allEvents,
required Events initialEvents,
CalendarEventData<CalendarDayEvent>? newEvent,
required List<String> deleteEventIds,
CalendarEventData<CalendarDayEvent>? updateEvent,
required Option<CalendarLayoutSettingsPB> settings,
required DatabaseLoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _CalendarState;
factory CalendarState.initial(String databaseId) => CalendarState(
factory CalendarState.initial() => CalendarState(
database: none(),
databaseId: databaseId,
fieldInfoByFieldId: {},
events: [],
allEvents: [],
initialEvents: [],
deleteEventIds: [],
settings: none(),
noneOrError: none(),
loadingState: const _Loading(),
@ -277,8 +374,10 @@ class CalendarEditingRow {
});
}
class CalendarCardData {
class CalendarDayEvent {
final CalendarEventPB event;
final CellIdentifier cellId;
CalendarCardData({required this.cellId, required this.event});
String get eventId => cellId.rowId;
CalendarDayEvent({required this.cellId, required this.event});
}

View File

@ -0,0 +1,55 @@
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'calendar_setting_bloc.freezed.dart';
typedef DayOfWeek = int;
class CalendarSettingBloc
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings})
: super(CalendarSettingState.initial(layoutSettings)) {
on<CalendarSettingEvent>((event, emit) {
event.when(
performAction: (action) {
emit(state.copyWith(selectedAction: Some(action)));
},
updateLayoutSetting: (setting) {
emit(state.copyWith(layoutSetting: Some(setting)));
},
);
});
}
@override
Future<void> close() async => super.close();
}
@freezed
class CalendarSettingState with _$CalendarSettingState {
const factory CalendarSettingState({
required Option<CalendarSettingAction> selectedAction,
required Option<CalendarLayoutSettingsPB> layoutSetting,
}) = _CalendarSettingState;
factory CalendarSettingState.initial(
CalendarLayoutSettingsPB? layoutSettings) =>
CalendarSettingState(
selectedAction: none(),
layoutSetting: layoutSettings == null ? none() : Some(layoutSettings),
);
}
@freezed
class CalendarSettingEvent with _$CalendarSettingEvent {
const factory CalendarSettingEvent.performAction(
CalendarSettingAction action) = _PerformAction;
const factory CalendarSettingEvent.updateLayoutSetting(
CalendarLayoutSettingsPB setting) = _UpdateLayoutSetting;
}
enum CalendarSettingAction {
layout,
}

View File

@ -34,7 +34,7 @@ class CalendarPluginBuilder extends PluginBuilder {
class CalendarPluginConfig implements PluginConfig {
@override
bool get creatable => false;
bool get creatable => true;
}
class CalendarPlugin extends Plugin {

View File

@ -0,0 +1,267 @@
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.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:provider/provider.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../application/calendar_bloc.dart';
class CalendarDayCard extends StatelessWidget {
final String viewId;
final bool isToday;
final bool isInMonth;
final DateTime date;
final RowCache _rowCache;
final CardCellBuilder _cellBuilder;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
CalendarDayCard({
required this.viewId,
required this.isToday,
required this.isInMonth,
required this.date,
required this.onCreateEvent,
required RowCache rowCache,
required this.events,
Key? key,
}) : _rowCache = rowCache,
_cellBuilder = CardCellBuilder(rowCache.cellCache),
super(key: key);
@override
Widget build(BuildContext context) {
Color backgroundColor = Theme.of(context).colorScheme.surface;
if (!isInMonth) {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
}
return ChangeNotifierProvider(
create: (_) => _CardEnterNotifier(),
builder: ((context, child) {
final children = events.map((event) {
return _DayEventCell(
event: event,
viewId: viewId,
onClick: () => _showRowDetailPage(event, context),
child: _cellBuilder.buildCell(
cellId: event.cellId,
styles: {FieldType.RichText: TextCardCellStyle(10)},
),
);
}).toList();
final child = Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_Header(
date: date,
isInMonth: isInMonth,
isToday: isToday,
onCreate: () => onCreateEvent(date),
),
VSpace(GridSize.typeOptionSeparatorHeight),
Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) {
return children[index];
},
itemCount: children.length,
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
),
),
],
));
return Container(
color: backgroundColor,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
child: child,
),
);
}),
);
}
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.cellId.rowId,
viewId: viewId,
rowCache: _rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: _rowCache.cellCache,
),
dataController: dataController,
);
},
);
}
notifyEnter(BuildContext context, bool isEnter) {
Provider.of<_CardEnterNotifier>(
context,
listen: false,
).onEnter = isEnter;
}
}
class _DayEventCell extends StatelessWidget {
final String viewId;
final CalendarDayEvent event;
final VoidCallback onClick;
final Widget child;
const _DayEventCell({
required this.viewId,
required this.event,
required this.onClick,
required this.child,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyHover(
child: GestureDetector(
onTap: onClick,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: child,
),
),
);
}
}
class _Header extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final VoidCallback onCreate;
const _Header({
required this.isToday,
required this.isInMonth,
required this.date,
required this.onCreate,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<_CardEnterNotifier>(
builder: (context, notifier, _) {
final badge = _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
date: date,
);
return Row(
children: [
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
const Spacer(),
badge,
],
);
},
);
}
}
class _NewEventButton extends StatelessWidget {
final VoidCallback onClick;
const _NewEventButton({
required this.onClick,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
onPressed: onClick,
iconPadding: EdgeInsets.zero,
icon: svgWidget(
"home/add",
color: Theme.of(context).colorScheme.onSurface,
),
width: 22,
);
}
}
class _DayBadge extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
const _DayBadge({
required this.isToday,
required this.isInMonth,
required this.date,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onSurface;
String dayString = date.day == 1
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
: date.day.toString();
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
}
Widget day = Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
padding: GridSize.typeOptionContentInsets,
child: FlowyText.medium(
dayString,
color: dayTextColor,
),
);
return day;
}
}
class _CardEnterNotifier extends ChangeNotifier {
bool _onEnter = false;
_CardEnterNotifier();
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -1,22 +1,15 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.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 'package:provider/provider.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart';
@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget {
}
class _CalendarPageState extends State<CalendarPage> {
final _eventController = EventController<CalendarCardData>();
final _eventController = EventController<CalendarDayEvent>();
GlobalKey<MonthViewState>? _calendarState;
late CalendarBloc _calendarBloc;
@ -58,21 +51,55 @@ class _CalendarPageState extends State<CalendarPage> {
value: _calendarBloc,
)
],
child: BlocListener<CalendarBloc, CalendarState>(
listenWhen: (previous, current) => previous.events != current.events,
listener: (context, state) {
if (state.events.isNotEmpty) {
_eventController.removeWhere((element) => true);
_eventController.addAll(state.events);
}
},
child: MultiBlocListener(
listeners: [
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.initialEvents != c.initialEvents,
listener: (context, state) {
_eventController.removeWhere((_) => true);
_eventController.addAll(state.initialEvents);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds,
listener: (context, state) {
_eventController.removeWhere(
(element) =>
state.deleteEventIds.contains(element.event!.eventId),
);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.updateEvent != c.updateEvent,
listener: (context, state) {
if (state.updateEvent != null) {
_eventController.removeWhere((element) =>
state.updateEvent!.event!.eventId ==
element.event!.eventId);
_eventController.add(state.updateEvent!);
}
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.newEvent != c.newEvent,
listener: (context, state) {
if (state.newEvent != null) {
_eventController.add(state.newEvent!);
}
},
),
],
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
return Column(
children: [
// const _ToolbarBlocAdaptor(),
_toolbar(),
_buildCalendar(_eventController),
const CalendarToolbar(),
_buildCalendar(
_eventController,
state.settings
.foldLeft(0, (previous, a) => a.firstDayOfWeek),
),
],
);
},
@ -82,16 +109,13 @@ class _CalendarPageState extends State<CalendarPage> {
);
}
Widget _toolbar() {
return const CalendarToolbar();
}
Widget _buildCalendar(EventController eventController) {
Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
return Expanded(
child: MonthView(
key: _calendarState,
controller: _eventController,
cellAspectRatio: 1.75,
cellAspectRatio: .9,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
@ -154,47 +178,19 @@ class _CalendarPageState extends State<CalendarPage> {
Widget _calendarDayBuilder(
DateTime date,
List<CalendarEventData<CalendarCardData>> calenderEvents,
List<CalendarEventData<CalendarDayEvent>> calenderEvents,
isToday,
isInMonth,
) {
final builder = CardCellBuilder(_calendarBloc.cellCache);
final cells = calenderEvents.map((value) => value.event!).map((event) {
final child = builder.buildCell(cellId: event.cellId);
final events = calenderEvents.map((value) => value.event!).toList();
return FlowyHover(
child: GestureDetector(
onTap: () {
final dataController = RowController(
rowId: event.cellId.rowId,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder:
GridCellBuilder(cellCache: _calendarBloc.cellCache),
dataController: dataController,
);
},
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: child,
),
),
);
}).toList();
return _CalendarCard(
return CalendarDayCard(
viewId: widget.view.id,
isToday: isToday,
isInMonth: isInMonth,
events: events,
date: date,
children: cells,
rowCache: _calendarBloc.rowCache,
onCreateEvent: (date) {
_calendarBloc.add(
CalendarEvent.createEvent(
@ -205,175 +201,9 @@ class _CalendarPageState extends State<CalendarPage> {
},
);
}
}
class _CalendarCard extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final List<Widget> children;
final void Function(DateTime) onCreateEvent;
const _CalendarCard({
required this.isToday,
required this.isInMonth,
required this.date,
required this.children,
required this.onCreateEvent,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Color backgroundColor = Theme.of(context).colorScheme.surface;
if (!isInMonth) {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
}
return ChangeNotifierProvider(
create: (_) => _CardEnterNotifier(),
builder: ((context, child) {
return Container(
color: backgroundColor,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_Header(
date: date,
isInMonth: isInMonth,
isToday: isToday,
onCreate: () => onCreateEvent(date),
),
...children
],
),
),
),
);
}),
);
}
notifyEnter(BuildContext context, bool isEnter) {
Provider.of<_CardEnterNotifier>(
context,
listen: false,
).onEnter = isEnter;
WeekDays _weekdayFromInt(int dayOfWeek) {
// MonthView places the first day of week on the second column for some reason.
return WeekDays.values[(dayOfWeek + 1) % 7];
}
}
class _Header extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final VoidCallback onCreate;
const _Header({
required this.isToday,
required this.isInMonth,
required this.date,
required this.onCreate,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<_CardEnterNotifier>(
builder: (context, notifier, _) {
final badge = _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
date: date,
);
return Row(
children: [
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
const Spacer(),
badge,
],
);
},
);
}
}
class _NewEventButton extends StatelessWidget {
final VoidCallback onClick;
const _NewEventButton({
required this.onClick,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
onPressed: onClick,
iconPadding: EdgeInsets.zero,
icon: svgWidget(
"home/add",
color: Theme.of(context).colorScheme.onSurface,
),
width: 22,
);
}
}
class _DayBadge extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
const _DayBadge({
required this.isToday,
required this.isInMonth,
required this.date,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onSurface;
String dayString = date.day == 1
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
: date.day.toString();
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
}
Widget day = Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
padding: GridSize.typeOptionContentInsets,
child: FlowyText.medium(
dayString,
color: dayTextColor,
),
);
return day;
}
}
class _CardEnterNotifier extends ChangeNotifier {
bool _onEnter = false;
_CardEnterNotifier();
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -0,0 +1,410 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'
hide DateFormat;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import 'calendar_setting.dart';
class CalendarLayoutSetting extends StatefulWidget {
final CalendarSettingContext settingContext;
final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
const CalendarLayoutSetting({
required this.onUpdated,
required this.settingContext,
super.key,
});
@override
State<CalendarLayoutSetting> createState() => _CalendarLayoutSettingState();
}
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
late final PopoverMutex popoverMutex;
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
builder: (context, state) {
final CalendarLayoutSettingsPB? settings = state.layoutSetting
.foldLeft(null, (previous, settings) => settings);
if (settings == null) {
return const CircularProgressIndicator();
}
final availableSettings = _availableCalendarSettings(settings);
final items = availableSettings.map((setting) {
switch (setting) {
case CalendarLayoutSettingAction.showWeekNumber:
return ShowWeekNumber(
showWeekNumbers: settings.showWeekNumbers,
onUpdated: (showWeekNumbers) {
_updateLayoutSettings(
context,
showWeekNumbers: showWeekNumbers,
onUpdated: widget.onUpdated,
);
},
);
case CalendarLayoutSettingAction.showWeekends:
return ShowWeekends(
showWeekends: settings.showWeekends,
onUpdated: (showWeekends) {
_updateLayoutSettings(
context,
showWeekends: showWeekends,
onUpdated: widget.onUpdated,
);
},
);
case CalendarLayoutSettingAction.firstDayOfWeek:
return FirstDayOfWeek(
firstDayOfWeek: settings.firstDayOfWeek,
popoverMutex: popoverMutex,
onUpdated: (firstDayOfWeek) {
_updateLayoutSettings(
context,
onUpdated: widget.onUpdated,
firstDayOfWeek: firstDayOfWeek,
);
},
);
case CalendarLayoutSettingAction.layoutField:
return LayoutDateField(
fieldController: widget.settingContext.fieldController,
viewId: widget.settingContext.viewId,
fieldId: settings.layoutFieldId,
popoverMutex: popoverMutex,
onUpdated: (fieldId) {
_updateLayoutSettings(context,
onUpdated: widget.onUpdated, layoutFieldId: fieldId);
},
);
default:
return ShowWeekends(
showWeekends: settings.showWeekends,
onUpdated: (showWeekends) {
_updateLayoutSettings(context,
onUpdated: widget.onUpdated, showWeekends: showWeekends);
},
);
}
}).toList();
return SizedBox(
width: 200,
child: ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: items.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) => items[index],
padding: const EdgeInsets.all(6.0),
),
);
},
);
}
List<CalendarLayoutSettingAction> _availableCalendarSettings(
CalendarLayoutSettingsPB layoutSettings) {
List<CalendarLayoutSettingAction> settings = [
CalendarLayoutSettingAction.layoutField,
// CalendarLayoutSettingAction.layoutType,
// CalendarLayoutSettingAction.showWeekNumber,
];
switch (layoutSettings.layoutTy) {
case CalendarLayoutPB.DayLayout:
// settings.add(CalendarLayoutSettingAction.showTimeLine);
break;
case CalendarLayoutPB.MonthLayout:
settings.addAll([
// CalendarLayoutSettingAction.showWeekends,
// if (layoutSettings.showWeekends)
CalendarLayoutSettingAction.firstDayOfWeek,
]);
break;
case CalendarLayoutPB.WeekLayout:
settings.addAll([
// CalendarLayoutSettingAction.showWeekends,
// if (layoutSettings.showWeekends)
CalendarLayoutSettingAction.firstDayOfWeek,
// CalendarLayoutSettingAction.showTimeLine,
]);
break;
}
return settings;
}
void _updateLayoutSettings(
BuildContext context, {
required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated,
bool? showWeekends,
bool? showWeekNumbers,
int? firstDayOfWeek,
String? layoutFieldId,
}) {
CalendarLayoutSettingsPB setting = context
.read<CalendarSettingBloc>()
.state
.layoutSetting
.foldLeft(null, (previous, settings) => settings)!;
setting.freeze();
setting = setting.rebuild((setting) {
if (showWeekends != null) {
setting.showWeekends = !showWeekends;
}
if (showWeekNumbers != null) {
setting.showWeekNumbers = !showWeekNumbers;
}
if (firstDayOfWeek != null) {
setting.firstDayOfWeek = firstDayOfWeek;
}
if (layoutFieldId != null) {
setting.layoutFieldId = layoutFieldId;
}
});
context
.read<CalendarSettingBloc>()
.add(CalendarSettingEvent.updateLayoutSetting(setting));
onUpdated(setting);
}
}
class LayoutDateField extends StatelessWidget {
final String fieldId;
final String viewId;
final FieldController fieldController;
final PopoverMutex popoverMutex;
final Function(String fieldId) onUpdated;
const LayoutDateField({
required this.fieldId,
required this.fieldController,
required this.viewId,
required this.popoverMutex,
required this.onUpdated,
super.key,
});
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(300, 400)),
mutex: popoverMutex,
popupBuilder: (context) {
return BlocProvider(
create: (context) => getIt<DatabasePropertyBloc>(
param1: viewId, param2: fieldController)
..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, state) {
final items = state.fieldContexts
.where((field) => field.fieldType == FieldType.DateTime)
.map(
(fieldInfo) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(fieldInfo.name),
onTap: () {
onUpdated(fieldInfo.id);
popoverMutex.close();
},
leftIcon: svgWidget('grid/field/date'),
rightIcon: fieldInfo.id == fieldId
? svgWidget('grid/checkmark')
: null,
),
);
},
).toList();
return SizedBox(
width: 200,
child: ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: items.length,
),
);
},
),
);
},
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
text: FlowyText.medium(
LocaleKeys.calendar_settings_layoutDateField.tr()),
),
),
);
}
}
class ShowWeekNumber extends StatelessWidget {
final bool showWeekNumbers;
final Function(bool showWeekNumbers) onUpdated;
const ShowWeekNumber({
required this.showWeekNumbers,
required this.onUpdated,
super.key,
});
@override
Widget build(BuildContext context) {
return _toggleItem(
onToggle: (showWeekNumbers) {
onUpdated(!showWeekNumbers);
},
value: showWeekNumbers,
text: LocaleKeys.calendar_settings_showWeekNumbers.tr(),
);
}
}
class ShowWeekends extends StatelessWidget {
final bool showWeekends;
final Function(bool showWeekends) onUpdated;
const ShowWeekends({
super.key,
required this.showWeekends,
required this.onUpdated,
});
@override
Widget build(BuildContext context) {
return _toggleItem(
onToggle: (showWeekends) {
onUpdated(!showWeekends);
},
value: showWeekends,
text: LocaleKeys.calendar_settings_showWeekends.tr(),
);
}
}
class FirstDayOfWeek extends StatelessWidget {
final int firstDayOfWeek;
final PopoverMutex popoverMutex;
final Function(int firstDayOfWeek) onUpdated;
const FirstDayOfWeek({
super.key,
required this.firstDayOfWeek,
required this.onUpdated,
required this.popoverMutex,
});
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(300, 400)),
mutex: popoverMutex,
popupBuilder: (context) {
final symbols =
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
// starts from sunday
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
final index = (entry.key - 1) % 7;
final string = entry.value;
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(string),
onTap: () {
onUpdated(index);
popoverMutex.close();
},
rightIcon:
firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
),
);
}).toList();
return SizedBox(
width: 100,
child: ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: 2,
),
);
},
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
text: FlowyText.medium(
LocaleKeys.calendar_settings_firstDayOfWeek.tr()),
),
),
);
}
}
Widget _toggleItem({
required String text,
required bool value,
required void Function(bool) onToggle,
}) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
child: Row(
children: [
FlowyText.medium(text),
const Spacer(),
Toggle(
value: value,
onChanged: (value) => onToggle(!value),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
);
}
enum CalendarLayoutSettingAction {
layoutField,
layoutType,
showWeekends,
firstDayOfWeek,
showWeekNumber,
showTimeLine,
}

View File

@ -0,0 +1,112 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import 'calendar_layout_setting.dart';
/// The highest-level widget shown in the popover triggered by clicking the
/// "Settings" button. By default, shows [AllCalendarSettings] but upon
/// selecting a category, replaces contents with contents of the submenu.
class CalendarSetting extends StatelessWidget {
final CalendarSettingContext settingContext;
final CalendarLayoutSettingsPB? layoutSettings;
final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
const CalendarSetting({
required this.onUpdated,
required this.layoutSettings,
required this.settingContext,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocProvider<CalendarSettingBloc>(
create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings),
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
builder: (context, state) {
final CalendarSettingAction? action =
state.selectedAction.foldLeft(null, (previous, action) => action);
switch (action) {
case CalendarSettingAction.layout:
return CalendarLayoutSetting(
onUpdated: onUpdated,
settingContext: settingContext,
);
default:
return const AllCalendarSettings().padding(all: 6.0);
}
},
),
);
}
}
/// Shows all of the available categories of settings that can be set here.
/// For now, this only includes the Layout category.
class AllCalendarSettings extends StatelessWidget {
const AllCalendarSettings({super.key});
@override
Widget build(BuildContext context) {
final items = CalendarSettingAction.values
.map((e) => _settingItem(context, e))
.toList();
return SizedBox(
width: 140,
child: ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: items.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) => items[index],
),
);
}
Widget _settingItem(BuildContext context, CalendarSettingAction action) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(action.title()),
onTap: () {
context
.read<CalendarSettingBloc>()
.add(CalendarSettingEvent.performAction(action));
},
),
);
}
}
extension _SettingExtension on CalendarSettingAction {
String title() {
switch (this) {
case CalendarSettingAction.layout:
return LocaleKeys.grid_settings_layout.tr();
}
}
}
class CalendarSettingContext {
final String viewId;
final FieldController fieldController;
CalendarSettingContext({
required this.viewId,
required this.fieldController,
});
}

View File

@ -1,5 +1,14 @@
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/calendar_bloc.dart';
import 'calendar_setting.dart';
class CalendarToolbar extends StatelessWidget {
const CalendarToolbar({super.key});
@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget {
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: const [
FlowyTextButton(
"Settings",
fillColor: Colors.transparent,
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
),
children: [
_SettingButton(),
],
),
);
}
}
class _SettingButton extends StatefulWidget {
@override
State<StatefulWidget> createState() => _SettingButtonState();
}
class _SettingButtonState extends State<_SettingButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithRightAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(300, 400)),
margin: EdgeInsets.zero,
child: FlowyTextButton(
LocaleKeys.settings_title.tr(),
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () => popoverController.show(),
),
popupBuilder: (BuildContext popoverContext) {
final bloc = context.watch<CalendarBloc>();
final settingContext = CalendarSettingContext(
viewId: bloc.viewId,
fieldController: bloc.fieldController,
);
return CalendarSetting(
settingContext: settingContext,
layoutSettings: bloc.state.settings.fold(
() => null,
(settings) => settings,
),
onUpdated: (layoutSettings) {
if (layoutSettings == null) {
return;
}
context
.read<CalendarBloc>()
.add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings));
},
);
}, // use blocbuilder
);
}
}

View File

@ -72,7 +72,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
add(GridEvent.didReceiveGridUpdate(database));
}
},
onRowsChanged: (rowInfos, reason) {
onRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
}

View File

@ -87,7 +87,7 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
child: BlocBuilder<CheckboxFilterEditorBloc, CheckboxFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPannel(context, state),
_buildFilterPanel(context, state),
];
return Padding(
@ -99,7 +99,7 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
);
}
Widget _buildFilterPannel(
Widget _buildFilterPanel(
BuildContext context, CheckboxFilterEditorState state) {
return SizedBox(
height: 20,

View File

@ -96,7 +96,7 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
SelectOptionFilterEditorState>(
builder: (context, state) {
List<Widget> slivers = [
SliverToBoxAdapter(child: _buildFilterPannel(context, state)),
SliverToBoxAdapter(child: _buildFilterPanel(context, state)),
];
if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty &&
@ -131,7 +131,7 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
);
}
Widget _buildFilterPannel(
Widget _buildFilterPanel(
BuildContext context, SelectOptionFilterEditorState state) {
return SizedBox(
height: 20,

View File

@ -94,7 +94,7 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPannel(context, state),
_buildFilterPanel(context, state),
];
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
@ -112,7 +112,7 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
);
}
Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) {
Widget _buildFilterPanel(BuildContext context, TextFilterEditorState state) {
return SizedBox(
height: 20,
child: Row(

View File

@ -147,6 +147,8 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
widget.popoverMutex.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
} else {
focusNode.requestFocus();
}
});

View File

@ -122,7 +122,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
color: Theme.of(context).colorScheme.background,
height: fixHeight,
child: FlowyTextField(
hintText: LocaleKeys.grid_settings_filterBy.tr(),
hintText: LocaleKeys.grid_settings_sortBy.tr(),
onChanged: (text) {
context
.read<CreateSortBloc>()

View File

@ -23,6 +23,7 @@ class CardCellBuilder<CustomCardData> {
required CellIdentifier cellId,
EditableCardNotifier? cellNotifier,
CardConfiguration<CustomCardData>? cardConfiguration,
Map<FieldType, CardCellStyle>? styles,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellId: cellId,
@ -30,6 +31,7 @@ class CardCellBuilder<CustomCardData> {
);
final key = cellId.key();
final style = styles?[cellId.fieldType];
switch (cellId.fieldType) {
case FieldType.Checkbox:
return CheckboxCardCell(
@ -70,6 +72,7 @@ class CardCellBuilder<CustomCardData> {
return TextCardCell(
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
style: isStyleOrNull<TextCardCellStyle>(style),
key: key,
);
case FieldType.URL:

View File

@ -24,10 +24,21 @@ class CardConfiguration<CustomCardData> {
}
}
abstract class CardCell<T> extends StatefulWidget {
final T? cardData;
abstract class CardCellStyle {}
const CardCell({super.key, this.cardData});
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
abstract class CardCell<T, S extends CardCellStyle> extends StatefulWidget {
final T? cardData;
final S? style;
const CardCell({super.key, this.cardData, this.style});
}
class EditableCardNotifier {

View File

@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/select_option_card_cell_bloc.dart';
import 'card_cell.dart';
class SelectOptionCardCell<T> extends CardCell<T> with EditableCell {
class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;

View File

@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class TextCardCell extends CardCell with EditableCell {
class TextCardCellStyle extends CardCellStyle {
final double fontSize;
TextCardCellStyle(this.fontSize);
}
class TextCardCell extends CardCell<String, TextCardCellStyle>
with EditableCell {
@override
final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder;
@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell {
const TextCardCell({
required this.cellControllerBuilder,
this.editableNotifier,
TextCardCellStyle? style,
Key? key,
}) : super(key: key);
}) : super(key: key, style: style);
@override
State<TextCardCell> createState() => _TextCardCellState();
@ -129,6 +136,14 @@ class _TextCardCellState extends State<TextCardCell> {
super.dispose();
}
double _fontSize() {
if (widget.style != null) {
return widget.style!.fontSize;
} else {
return 14;
}
}
Widget _buildText(TextCardCellState state) {
return Padding(
padding: EdgeInsets.symmetric(
@ -136,7 +151,7 @@ class _TextCardCellState extends State<TextCardCell> {
),
child: FlowyText.medium(
state.content,
fontSize: 14,
fontSize: _fontSize(),
maxLines: null, // Enable multiple lines
),
);
@ -150,7 +165,7 @@ class _TextCardCellState extends State<TextCardCell> {
onChanged: (value) => focusChanged(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()),
decoration: InputDecoration(
// Magic number 4 makes the textField take up the same space as FlowyText
contentPadding: EdgeInsets.symmetric(

View File

@ -23,9 +23,8 @@ import '../../../../grid/presentation/widgets/common/type_option_separator.dart'
import '../../../../grid/presentation/widgets/header/type_option/date.dart';
import 'date_cal_bloc.dart';
final kToday = DateTime.now();
final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
final kFirstDay = DateTime.utc(1970, 1, 1);
final kLastDay = DateTime.utc(2100, 1, 1);
class DateCellEditor extends StatefulWidget {
final VoidCallback onDismissed;

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/plugins/document/application/share_service.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -53,6 +54,7 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
return documentToMarkdown(document, customParsers: [
const DividerNodeParser(),
const MathEquationNodeParser(),
const CodeBlockNodeParser(),
]);
}
}

View File

@ -1,4 +1,7 @@
import 'package:appflowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
@ -7,19 +10,18 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../startup/startup.dart';
import 'application/doc_bloc.dart';
import 'editor_styles.dart';
import 'presentation/banner.dart';
import 'presentation/plugins/grid/grid_view_menu_item.dart';
import 'presentation/plugins/board/board_menu_item.dart';
class DocumentPage extends StatefulWidget {
final VoidCallback onDeleted;
@ -128,11 +130,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final autoFocusParamters = _autoFocusParamters();
final autoFocusParameters = _autoFocusParameters();
final editor = AppFlowyEditor(
editorState: editorState,
autoFocus: autoFocusParamters.value1,
focusedSelection: autoFocusParamters.value2,
autoFocus: autoFocusParameters.value1,
focusedSelection: autoFocusParameters.value2,
customBuilders: {
// Divider
kDividerType: DividerWidgetBuilder(),
@ -172,8 +174,12 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
emojiMenuItem,
// Board
boardMenuItem,
// Create Board
boardViewMenuItem(documentBloc),
// Grid
gridMenuItem,
// Create Grid
gridViewMenuItem(documentBloc),
// Callout
calloutMenuItem,
// AI
@ -234,7 +240,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
}
}
dartz.Tuple2<bool, Selection?> _autoFocusParamters() {
dartz.Tuple2<bool, Selection?> _autoFocusParameters() {
if (editorState.document.isEmpty) {
return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
}

View File

@ -82,8 +82,8 @@ Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
},
);
final pluginTheme = Theme.of(context).brightness == Brightness.dark
? darkPlguinStyleExtension
: lightPlguinStyleExtension;
? darkPluginStyleExtension
: lightPluginStyleExtension;
return pluginTheme.toList()
..removeWhere((element) =>
element is HeadingPluginStyle || element is NumberListPluginStyle)

View File

@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
const String _kDocumentAppearenceFontSize = 'kDocumentAppearenceFontSize';
const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize';
class DocumentAppearance {
const DocumentAppearance({
@ -24,7 +24,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
void fetch() async {
final prefs = await SharedPreferences.getInstance();
final fontSize = prefs.getDouble(_kDocumentAppearenceFontSize) ?? 14.0;
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 14.0;
emit(state.copyWith(
fontSize: fontSize,
));
@ -32,7 +32,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
void syncFontSize(double fontSize) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(_kDocumentAppearenceFontSize, fontSize);
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
emit(state.copyWith(
fontSize: fontSize,
));

View File

@ -20,7 +20,7 @@ void showLinkToPageMenu(
BuildContext context,
ViewLayoutTypePB pageType,
) {
final aligment = menuService.alignment;
final alignment = menuService.alignment;
final offset = menuService.offset;
menuService.dismiss();
@ -41,8 +41,8 @@ void showLinkToPageMenu(
_linkToPageMenu?.remove();
_linkToPageMenu = OverlayEntry(builder: (context) {
return Positioned(
top: aligment == Alignment.bottomLeft ? offset.dy : null,
bottom: aligment == Alignment.topLeft ? offset.dy : null,
top: alignment == Alignment.bottomLeft ? offset.dy : null,
bottom: alignment == Alignment.topLeft ? offset.dy : null,
left: offset.dx,
child: Material(
color: Colors.transparent,

View File

@ -17,7 +17,8 @@ SelectionMenuItem boardMenuItem = SelectionMenuItem(
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
keywords: ['board', 'kanban'],
// TODO(a-wallen): Translate keywords
keywords: ['referenced board', 'referenced kanban'],
handler: (editorState, menuService, context) {
showLinkToPageMenu(
editorState,

View File

@ -0,0 +1,61 @@
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
import 'package:appflowy/workspace/application/app/app_service.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem(
name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
icon: (editorState, onSelected) {
return svgWidget(
'editor/board',
size: const Size.square(18.0),
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
// TODO(a-wallen): Translate keywords.
keywords: ['board', 'kanban'],
handler: (editorState, menuService, context) async {
if (!documentBloc.view.hasAppId()) {
return;
}
final appId = documentBloc.view.appId;
final service = AppBackendService();
final result = (await service.createView(
appId: appId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutTypePB.Board,
))
.getLeftOrNull();
// If the result is null, then something went wrong here.
if (result == null) {
return;
}
final app =
(await service.readApp(appId: result.appId)).getLeftOrNull();
// We should show an error dialog.
if (app == null) {
return;
}
final view =
(await service.getView(result.appId, result.id)).getLeftOrNull();
// As this.
if (view == null) {
return;
}
editorState.insertPage(app, view);
},
);

View File

@ -1,3 +1,4 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
@ -25,6 +26,84 @@ class CoverImagePicker extends StatefulWidget {
}
class _CoverImagePickerState extends State<CoverImagePicker> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CoverImagePickerBloc()
..add(const CoverImagePickerEvent.initialEvent()),
child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(
listener: (context, state) {
if (state is NetworkImagePicked) {
state.successOrFail.isRight()
? showSnapBar(context,
LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
: null;
}
if (state is Done) {
state.successOrFail.fold(
(l) => widget.onFileSubmit(l),
(r) => showSnapBar(
context,
LocaleKeys.document_plugins_cover_failedToAddImageToGallery
.tr()));
}
},
child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state is Loading
? const SizedBox(
height: 180,
child: Center(
child: CircularProgressIndicator(),
),
)
: CoverImagePreviewWidget(state: state),
const SizedBox(
height: 10,
),
NetworkImageUrlInput(
onAdd: (url) {
context.read<CoverImagePickerBloc>().add(UrlSubmit(url));
},
),
const SizedBox(
height: 10,
),
ImagePickerActionButtons(
onBackPressed: () {
widget.onBackPressed();
},
onSave: () {
context.read<CoverImagePickerBloc>().add(
SaveToGallery(state),
);
},
),
],
);
},
),
),
);
}
}
class NetworkImageUrlInput extends StatefulWidget {
final void Function(String color) onAdd;
const NetworkImageUrlInput({
super.key,
required this.onAdd,
});
@override
State<NetworkImageUrlInput> createState() => _NetworkImageUrlInputState();
}
class _NetworkImageUrlInputState extends State<NetworkImageUrlInput> {
TextEditingController urlController = TextEditingController();
bool get buttonDisabled => urlController.text.isEmpty;
@ -36,6 +115,85 @@ class _CoverImagePickerState extends State<CoverImagePicker> {
});
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 4,
child: FlowyTextField(
controller: urlController,
hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(),
),
),
const SizedBox(
width: 5,
),
Expanded(
flex: 1,
child: RoundedTextButton(
onPressed: () {
urlController.text.isNotEmpty
? widget.onAdd(urlController.text)
: null;
},
hoverColor: Colors.transparent,
fillColor: buttonDisabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(),
borderRadius: Corners.s8Border,
),
)
],
);
}
}
class ImagePickerActionButtons extends StatelessWidget {
final VoidCallback onBackPressed;
final VoidCallback onSave;
const ImagePickerActionButtons(
{super.key, required this.onBackPressed, required this.onSave});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => onBackPressed(),
),
FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () => onSave(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary,
),
],
);
}
}
class CoverImagePreviewWidget extends StatefulWidget {
final dynamic state;
const CoverImagePreviewWidget({super.key, required this.state});
@override
State<CoverImagePreviewWidget> createState() =>
_CoverImagePreviewWidgetState();
}
class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
_buildFilePickerWidget(BuildContext ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -105,150 +263,43 @@ class _CoverImagePickerState extends State<CoverImagePicker> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CoverImagePickerBloc()
..add(const CoverImagePickerEvent.initialEvent()),
child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(
listener: (context, state) {
if (state is NetworkImagePicked) {
state.successOrFail.isRight()
? showSnapBar(context,
LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
: null;
}
if (state is Done) {
state.successOrFail.fold(
(l) => widget.onFileSubmit(l),
(r) => showSnapBar(
context,
LocaleKeys.document_plugins_cover_failedToAddImageToGallery
.tr()));
}
},
child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state is Loading
? const SizedBox(
height: 180,
child: Center(
child: CircularProgressIndicator(),
return Stack(
children: [
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
borderRadius: Corners.s6Border,
image: widget.state is Initial
? null
: widget.state is NetworkImagePicked
? widget.state.successOrFail.fold(
(path) => DecorationImage(
image: NetworkImage(path), fit: BoxFit.cover),
(r) => null)
: widget.state is FileImagePicked
? DecorationImage(
image: FileImage(File(widget.state.path)),
fit: BoxFit.cover)
: null),
child: (widget.state is Initial)
? _buildFilePickerWidget(context)
: (widget.state is NetworkImagePicked)
? widget.state.successOrFail.fold(
(l) => null,
(r) => _buildFilePickerWidget(
context,
),
)
: Stack(
children: [
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondary,
borderRadius: Corners.s6Border,
image: state is Initial
? null
: state is NetworkImagePicked
? state.successOrFail.fold(
(path) => DecorationImage(
image: NetworkImage(path),
fit: BoxFit.cover),
(r) => null)
: state is FileImagePicked
? DecorationImage(
image: FileImage(
File(state.path)),
fit: BoxFit.cover)
: null),
child: (state is Initial)
? _buildFilePickerWidget(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => null,
(r) => _buildFilePickerWidget(
context,
),
)
: null),
(state is FileImagePicked)
? _buildImageDeleteButton(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => _buildImageDeleteButton(context),
(r) => Container())
: Container()
],
),
const SizedBox(
height: 10,
),
Row(
children: [
Expanded(
flex: 4,
child: FlowyTextField(
controller: urlController,
hintText: LocaleKeys
.document_plugins_cover_enterImageUrl
.tr(),
),
),
const SizedBox(
width: 5,
),
Expanded(
flex: 1,
child: RoundedTextButton(
onPressed: () {
urlController.text.isNotEmpty
? context
.read<CoverImagePickerBloc>()
.add(UrlSubmit(urlController.text))
: null;
},
hoverColor: Colors.transparent,
fillColor: buttonDisabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(),
borderRadius: Corners.s8Border,
),
)
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => widget.onBackPressed(),
),
FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () async {
context
.read<CoverImagePickerBloc>()
.add(SaveToGallery(state));
},
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary,
),
],
)
],
);
},
),
),
: null),
(widget.state is FileImagePicked)
? _buildImageDeleteButton(context)
: (widget.state is NetworkImagePicked)
? widget.state.successOrFail.fold(
(l) => _buildImageDeleteButton(context), (r) => Container())
: Container()
],
);
}
}

View File

@ -463,7 +463,7 @@ class _CoverImageState extends State<_CoverImage> {
coverImage = const SizedBox();
break;
}
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
return SizedBox(
height: height,
child: OverflowBox(

View File

@ -17,7 +17,7 @@ SelectionMenuItem gridMenuItem = SelectionMenuItem(
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
keywords: ['grid'],
keywords: ['referenced grid'],
handler: (editorState, menuService, context) {
showLinkToPageMenu(
editorState,

View File

@ -0,0 +1,60 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
import 'package:appflowy/workspace/application/app/app_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
SelectionMenuItem(
name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
icon: (editorState, onSelected) {
return svgWidget(
'editor/grid',
size: const Size.square(18.0),
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
);
},
keywords: ['grid'],
handler: (editorState, menuService, context) async {
if (!documentBloc.view.hasAppId()) {
return;
}
final appId = documentBloc.view.appId;
final service = AppBackendService();
final result = (await service.createView(
appId: appId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutTypePB.Grid,
))
.getLeftOrNull();
// If the result is null, then something went wrong here.
if (result == null) {
return;
}
final app =
(await service.readApp(appId: result.appId)).getLeftOrNull();
// We should show an error dialog.
if (app == null) {
return;
}
final view =
(await service.getView(result.appId, result.id)).getLeftOrNull();
// As this.
if (view == null) {
return;
}
editorState.insertPage(app, view);
},
);

View File

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'text_completion.dart';
import 'package:dartz/dartz.dart';
@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final parameters = {
'model': 'text-davinci-003',
@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
syntax += 1;
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
if (!useAction) {
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
}
} else {
if (syntax == 2) {
await onStart();
continue;
} else if (syntax < 2) {
continue;
}
}
final data = chunk.trim().split('data: ');
Log.editor.info(data.toString());
if (data.length > 1) {
if (data[1] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
previousSyntax = response.choices.first.text;
}
} else {
onEnd();
await onEnd();
}
}
}
@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
OpenAIError.fromJson(json.decode(body)['error']),
);
}
return;
}
@override

View File

@ -0,0 +1,9 @@
import 'package:url_launcher/url_launcher.dart';
Future<void> openLearnMorePage() async {
final uri = Uri.parse(
'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -9,7 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart';
import 'package:http/http.dart' as http;
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
final controller = TextEditingController();
final focusNode = FocusNode();
final textFieldFocusNode = FocusNode();
final interceptor = SelectionInterceptor();
@override
void initState() {
@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.addListener(_onFocusChanged);
textFieldFocusNode.requestFocus();
widget.editorState.service.selectionService.register(interceptor
..canTap = (details) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox != null) {
if (!isTapDownDetailsInRenderBox(details, renderBox)) {
if (text.isNotEmpty || controller.text.isNotEmpty) {
showDialog(
context: context,
builder: (context) {
return DiscardDialog(
onConfirm: () => _onDiscard(),
onCancel: () {},
);
},
);
} else if (controller.text.isEmpty) {
_onExit();
}
}
}
return false;
});
}
bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
var result = BoxHitTestResult();
box.hitTest(result, position: box.globalToLocal(details.globalPosition));
return result.path.any((entry) => entry.target == box);
}
@override
@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.removeListener(_onFocusChanged);
widget.editorState.service.selectionService.currentSelection
.removeListener(_onCancelWhenSelectionChanged);
widget.editorState.service.selectionService.unRegister(interceptor);
super.dispose();
}
@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
fontSize: 14,
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
)
],
);
}
Widget _buildInputWidget(BuildContext context) {
return RawKeyboardListener(
focusNode: focusNode,
onKey: (RawKeyEvent event) async {
if (event is! RawKeyDownEvent) return;
if (event.logicalKey == LogicalKeyboardKey.enter) {
if (controller.text.isNotEmpty) {
textFieldFocusNode.unfocus();
await _onGenerate();
}
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
await _onExit();
}
},
child: FlowyTextField(
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
controller: controller,
maxLines: 3,
focusNode: textFieldFocusNode,
autoFocus: false,
),
return FlowyTextField(
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
controller: controller,
maxLines: 3,
focusNode: textFieldFocusNode,
autoFocus: false,
);
}
@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_generate.tr()} ',
text: LocaleKeys.button_generate.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () async => await _onGenerate(),
@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_Cancel.tr()} ',
text: LocaleKeys.button_Cancel.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: LocaleKeys.button_esc.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () async => await _onExit(),
),
Expanded(
child: Container(
alignment: Alignment.centerRight,
child: FlowyText.regular(
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
),
],
);
}

View File

@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
name: 'Auto Generator',
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
iconData: Icons.generating_tokens,
keywords: ['autogenerator', 'auto generator'],
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
nodeBuilder: (editorState) {
final node = Node(
type: kAutoCompletionInputType,

View File

@ -0,0 +1,28 @@
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class DiscardDialog extends StatelessWidget {
const DiscardDialog({
super.key,
required this.onConfirm,
required this.onCancel,
});
final VoidCallback onConfirm;
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: LocaleKeys.document_plugins_discardResponse.tr(),
okTitle: LocaleKeys.button_discard.tr(),
cancelTitle: LocaleKeys.button_Cancel.tr(),
onOkPressed: onConfirm,
onCancelPressed: onCancel,
);
}
}

View File

@ -17,7 +17,7 @@ class Loading {
return const SimpleDialog(
elevation: 0.0,
backgroundColor:
Colors.transparent, // can change this to your prefered color
Colors.transparent, // can change this to your preferred color
children: <Widget>[
Center(
child: CircularProgressIndicator(),

View File

@ -10,11 +10,39 @@ enum SmartEditAction {
String get toInstruction {
switch (this) {
case SmartEditAction.summarize:
return 'Make this shorter and more concise:';
return 'Tl;dr';
case SmartEditAction.fixSpelling:
return 'Correct this to standard English:';
}
}
String prompt(String input) {
switch (this) {
case SmartEditAction.summarize:
return '$input\n\nTl;dr';
case SmartEditAction.fixSpelling:
return 'Correct this to standard English:\n\n$input';
}
}
static SmartEditAction from(int index) {
switch (index) {
case 0:
return SmartEditAction.summarize;
case 1:
return SmartEditAction.fixSpelling;
}
return SmartEditAction.fixSpelling;
}
String get name {
switch (this) {
case SmartEditAction.summarize:
return LocaleKeys.document_plugins_smartEditSummarize.tr();
case SmartEditAction.fixSpelling:
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
}
}
}
class SmartEditActionWrapper extends ActionCell {
@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
@override
String get name {
switch (inner) {
case SmartEditAction.summarize:
return LocaleKeys.document_plugins_smartEditSummarize.tr();
case SmartEditAction.fixSpelling:
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
}
return inner.name;
}
}

View File

@ -1,19 +1,18 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
import 'dart:async';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart' as http;
import 'package:dartz/dartz.dart' as dartz;
import 'package:appflowy/util/either_extension.dart';
const String kSmartEditType = 'smart_edit_input';
const String kSmartEditInstructionType = 'smart_edit_instruction';
@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input';
class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
@override
NodeValidator<Node> get nodeValidator => (node) {
return SmartEditAction.values.map((e) => e.toInstruction).contains(
node.attributes[kSmartEditInstructionType],
) &&
return SmartEditAction.values
.map((e) => e.index)
.contains(node.attributes[kSmartEditInstructionType]) &&
node.attributes[kSmartEditInputType] is String;
};
@override
Widget build(NodeWidgetContext<Node> context) {
return _SmartEditInput(
return _HoverSmartInput(
key: context.node.key,
node: context.node,
editorState: context.editorState,
@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
}
}
class _SmartEditInput extends StatefulWidget {
final Node node;
final EditorState editorState;
const _SmartEditInput({
Key? key,
class _HoverSmartInput extends StatefulWidget {
const _HoverSmartInput({
required super.key,
required this.node,
required this.editorState,
});
final Node node;
final EditorState editorState;
@override
State<_HoverSmartInput> createState() => _HoverSmartInputState();
}
class _HoverSmartInputState extends State<_HoverSmartInput> {
final popoverController = PopoverController();
final key = GlobalKey(debugLabel: 'smart_edit_input');
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
@override
Widget build(BuildContext context) {
final width = _maxWidth();
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
margin: EdgeInsets.zero,
constraints: BoxConstraints(maxWidth: width),
decoration: FlowyDecoration.decoration(
Colors.transparent,
Colors.transparent,
),
child: const SizedBox(
width: double.infinity,
),
canClose: () async {
final completer = Completer<bool>();
final state = key.currentState as _SmartEditInputState;
if (state.result.isEmpty) {
completer.complete(true);
} else {
showDialog(
context: context,
builder: (context) {
return DiscardDialog(
onConfirm: () => completer.complete(true),
onCancel: () => completer.complete(false),
);
},
);
}
return completer.future;
},
popupBuilder: (BuildContext popoverContext) {
return _SmartEditInput(
key: key,
node: widget.node,
editorState: widget.editorState,
);
},
);
}
double _maxWidth() {
var width = double.infinity;
final editorSize = widget.editorState.renderBox?.size;
final padding = widget.editorState.editorStyle.padding;
if (editorSize != null && padding != null) {
width = editorSize.width - padding.left - padding.right;
}
return width;
}
}
class _SmartEditInput extends StatefulWidget {
const _SmartEditInput({
required super.key,
required this.node,
required this.editorState,
});
final Node node;
final EditorState editorState;
@override
State<_SmartEditInput> createState() => _SmartEditInputState();
}
class _SmartEditInputState extends State<_SmartEditInput> {
String get instruction => widget.node.attributes[kSmartEditInstructionType];
SmartEditAction get action =>
SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
String get input => widget.node.attributes[kSmartEditInputType];
final focusNode = FocusNode();
final client = http.Client();
dartz.Either<OpenAIError, TextEditResponse>? result;
bool loading = true;
String result = '';
@override
void initState() {
@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
widget.editorState.service.keyboardService?.enable();
}
});
_requestEdits().then(
(value) => setState(() {
result = value;
loading = false;
}),
);
_requestCompletions();
}
@override
@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}
Widget _buildSmartEditPanel(BuildContext context) {
return RawKeyboardListener(
focusNode: focusNode,
onKey: (RawKeyEvent event) async {
if (event is! RawKeyDownEvent) return;
if (event.logicalKey == LogicalKeyboardKey.enter) {
await _onReplace();
await _onExit();
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
await _onExit();
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWidget(context),
const Space(0, 10),
_buildResultWidget(context),
const Space(0, 10),
_buildInputFooterWidget(context),
],
),
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWidget(context),
const Space(0, 10),
_buildResultWidget(context),
const Space(0, 10),
_buildInputFooterWidget(context),
],
);
}
@ -128,13 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return Row(
children: [
FlowyText.medium(
LocaleKeys.document_plugins_smartEditTitleName.tr(),
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
fontSize: 14,
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
)
],
);
}
@ -147,25 +218,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
child: const CircularProgressIndicator(),
),
);
if (result == null) {
if (result.isEmpty) {
return loading;
}
return result!.fold((error) {
return Flexible(
child: Text(
error.message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.red,
),
),
);
}, (response) {
return Flexible(
child: Text(
response.choices.map((e) => e.text).join('\n'),
),
);
});
return Flexible(
child: Text(
result,
),
);
}
Widget _buildInputFooterWidget(BuildContext context) {
@ -175,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_replace.tr()} ',
text: LocaleKeys.button_replace.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () {
_onReplace();
onPressed: () async {
await _onReplace();
_onExit();
},
),
@ -196,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_Cancel.tr()} ',
text: LocaleKeys.button_insertBelow.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
onPressed: () async {
await _onInsertBelow();
_onExit();
},
),
const Space(10, 0),
FlowyRichTextButton(
TextSpan(
children: [
TextSpan(
text: LocaleKeys.button_esc.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
text: LocaleKeys.button_Cancel.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
onPressed: () async => await _onExit(),
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
),
],
);
}
@ -219,12 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final selectedNodes = widget
.editorState.service.selectionService.currentSelectedNodes.normalized
.whereType<TextNode>();
if (selection == null || result == null || result!.isLeft()) {
if (selection == null || result.isEmpty) {
return;
}
final texts = result!.asRight().choices.first.text.split('\n')
..removeWhere((element) => element.isEmpty);
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
final transaction = widget.editorState.transaction;
transaction.replaceTexts(
selectedNodes.toList(growable: false),
@ -234,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return widget.editorState.apply(transaction);
}
Future<void> _onInsertBelow() async {
final selection = widget.editorState.service.selectionService
.currentSelection.value?.normalized;
if (selection == null || result.isEmpty) {
return;
}
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
final transaction = widget.editorState.transaction;
transaction.insertNodes(
selection.normalized.end.path.next,
texts.map(
(e) => TextNode(
delta: Delta()..insert(e),
),
),
);
return widget.editorState.apply(transaction);
}
Future<void> _onExit() async {
final transaction = widget.editorState.transaction;
transaction.deleteNode(widget.node);
@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> {
);
}
Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
Future<void> _requestCompletions() async {
final result = await UserBackendService.getCurrentUserProfile();
return result.fold((userProfile) async {
return result.fold((l) async {
final openAIRepository = HttpOpenAIRepository(
client: client,
apiKey: userProfile.openaiKey,
apiKey: l.openaiKey,
);
final edits = await openAIRepository.getEdits(
input: input,
instruction: instruction,
n: 1,
);
return edits.fold((error) async {
return dartz.Left(
OpenAIError(
message:
LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
),
var lines = input.split('\n\n');
if (action == SmartEditAction.summarize) {
lines = [lines.join('\n')];
}
for (var i = 0; i < lines.length; i++) {
final element = lines[i];
await openAIRepository.getStreamedCompletions(
useAction: true,
prompt: action.prompt(element),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (response) async {
setState(() {
this.result += response.choices.first.text;
});
},
onEnd: () async {
setState(() {
if (i != lines.length - 1) {
this.result += '\n';
}
});
},
onError: (error) async {
await _showError(error.message);
await _onExit();
},
);
}, (textEdit) async {
return dartz.Right(textEdit);
});
}, (error) async {
// error
return dartz.Left(
OpenAIError(
message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
),
);
}
}, (r) async {
await _showError(r.msg);
await _onExit();
});
}
Future<void> _showError(String message) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: LocaleKeys.button_Cancel.tr(),
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
content: FlowyText(message),
),
);
}
}

View File

@ -101,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
textNodes.normalized,
selection.normalized,
);
while (input.last.isEmpty) {
input.removeLast();
}
final transaction = widget.editorState.transaction;
transaction.insertNode(
selection.normalized.end.path.next,
Node(
type: kSmartEditType,
attributes: {
kSmartEditInstructionType: actionWrapper.inner.toInstruction,
kSmartEditInputType: input,
kSmartEditInstructionType: actionWrapper.inner.index,
kSmartEditInputType: input.join('\n\n'),
},
),
);

View File

@ -0,0 +1,13 @@
import 'package:appflowy_editor/appflowy_editor.dart';
class CodeBlockNodeParser extends NodeParser {
const CodeBlockNodeParser();
@override
String get id => 'code_block';
@override
String transform(Node node) {
return '```\n${node.attributes['code_block']}\n```';
}
}

View File

@ -27,7 +27,7 @@ import 'tasks/prelude.dart';
// AppWidgetTaskApplicationWidget SplashScreen
//
//
// 3.build MeterialApp
// 3.build MaterialApp
final getIt = GetIt.instance;
abstract class EntryPoint {

View File

@ -0,0 +1,24 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Debounce {
final Duration duration;
Timer? _timer;
Debounce({
this.duration = const Duration(milliseconds: 1000),
});
void call(VoidCallback action) {
dispose();
_timer = Timer(duration, () {
action();
});
}
void dispose() {
_timer?.cancel();
_timer = null;
}
}

View File

@ -141,7 +141,7 @@ class AppEvent with _$AppEvent {
PluginBuilder pluginBuilder, {
String? desc,
/// The initial data should be the JSON of the doucment
/// The initial data should be the JSON of the document
/// For example: {"document":{"type":"editor","children":[]}}
String? initialData,
Map<String, String>? ext,

View File

@ -55,9 +55,9 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
newLocale = const Locale('en');
}
context.setLocale(newLocale);
if (state.locale != newLocale) {
context.setLocale(newLocale);
_setting.locale.languageCode = newLocale.languageCode;
_setting.locale.countryCode = newLocale.countryCode ?? "";
_saveAppearanceSettings();

View File

@ -33,14 +33,14 @@ class ViewSection extends StatelessWidget {
},
child: BlocBuilder<ViewSectionBloc, ViewSectionState>(
builder: (context, state) {
return _reorderableColum(context, state);
return _reorderableColumn(context, state);
},
),
),
);
}
ReorderableColumn _reorderableColum(
ReorderableColumn _reorderableColumn(
BuildContext context, ViewSectionState state) {
final children = state.views.map((view) {
return ViewSectionItem(

View File

@ -12,6 +12,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
class SettingsDialog extends StatelessWidget {
final UserProfilePB user;
SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id));
@ -23,34 +26,46 @@ class SettingsDialog extends StatelessWidget {
..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
builder: (context, state) => FlowyDialog(
title: FlowyText(
LocaleKeys.settings_title.tr(),
fontSize: 20,
fontWeight: FontWeight.w700,
title: Padding(
padding: _dialogHorizontalPadding + _contentInsetPadding,
child: FlowyText(
LocaleKeys.settings_title.tr(),
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 200,
child: SettingsMenu(
changeSelectedPage: (index) {
context
.read<SettingsDialogBloc>()
.add(SettingsDialogEvent.setSelectedPage(index));
},
currentPage: context.read<SettingsDialogBloc>().state.page,
child: ScaffoldMessenger(
child: Scaffold(
backgroundColor: Colors.transparent,
body: Padding(
padding: _dialogHorizontalPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 200,
child: SettingsMenu(
changeSelectedPage: (index) {
context
.read<SettingsDialogBloc>()
.add(SettingsDialogEvent.setSelectedPage(index));
},
currentPage:
context.read<SettingsDialogBloc>().state.page,
),
),
const VerticalDivider(),
const SizedBox(width: 10),
Expanded(
child: getSettingsView(
context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.userProfile,
),
)
],
),
),
const VerticalDivider(),
const SizedBox(width: 10),
Expanded(
child: getSettingsView(
context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.userProfile,
),
)
],
),
),
),
),

View File

@ -46,7 +46,17 @@ class SettingsFileLocationCustomzierState
onDoubleTap: () {
Clipboard.setData(ClipboardData(
text: state.path,
));
)).then((_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText(
LocaleKeys.settings_files_pathCopiedSnackbar.tr(),
),
),
);
}
});
},
child: FlowyText.regular(
state.path ?? '',

View File

@ -54,38 +54,37 @@ class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) => {hoverEnterLanguage()},
onExit: (event) => {hoverExitLanguage()},
onEnter: (_) => hoverEnterLanguage(),
onExit: (_) => hoverExitLanguage(),
child: Container(
margin: const EdgeInsets.only(left: 8, right: 8),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: currHoverColor,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Locale>(
value: context.locale,
onChanged: (val) {
setState(() {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: DropdownButton<Locale>(
value: context.locale,
onChanged: (locale) {
context
.read<AppearanceSettingsCubit>()
.setLocale(context, val!);
});
},
icon: const Visibility(
visible: false,
child: (Icon(Icons.arrow_downward)),
.setLocale(context, locale!);
},
autofocus: true,
borderRadius: BorderRadius.circular(8),
items:
EasyLocalization.of(context)!.supportedLocales.map((locale) {
return DropdownMenuItem<Locale>(
value: locale,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: FlowyText.medium(languageFromLocale(locale)),
),
);
}).toList(),
),
borderRadius: BorderRadius.circular(8),
items: EasyLocalization.of(context)!.supportedLocales.map((locale) {
return DropdownMenuItem<Locale>(
value: locale,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: FlowyText.medium(languageFromLocale(locale)),
),
);
}).toList(),
),
),
),

View File

@ -1,4 +1,5 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget {
class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
bool visible = false;
final textEditingController = TextEditingController();
final debounce = Debounce();
@override
void initState() {
super.initState();
textEditingController.text = widget.openAIKey;
}
@override
Widget build(BuildContext context) {
return TextField(
controller: TextEditingController()..text = widget.openAIKey,
controller: textEditingController,
obscureText: !visible,
decoration: InputDecoration(
labelText: 'OpenAI Key',
@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
},
),
),
onSubmitted: (val) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserOpenAIKey(val));
onChanged: (value) {
debounce.call(() {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserOpenAIKey(value));
});
},
);
}
@override
void dispose() {
debounce.dispose();
super.dispose();
}
}
class _CurrentIcon extends StatelessWidget {

View File

@ -135,7 +135,6 @@ class FlowyVersionDescription extends CustomActionCell {
PackageInfo packageInfo = snapshot.data;
String appName = packageInfo.appName;
String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber;
return SizedBox(
height: 30,
@ -149,7 +148,7 @@ class FlowyVersionDescription extends CustomActionCell {
thickness: 1.0),
const VSpace(6),
FlowyText(
"$appName $version.$buildNumber",
"$appName $version",
color: Theme.of(context).hintColor,
),
],

View File

@ -1,8 +1,8 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "appflowy_flutter")
set(APPLICATION_ID "com.example.appflowy_flutter")
set(BINARY_NAME "AppFlowy")
set(APPLICATION_ID "io.appflowy.appflowy")
cmake_policy(SET CMP0063 NEW)

View File

@ -2,7 +2,7 @@
Name=AppFlowy
Comment=An Open Source Alternative to Notion
Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg
Exec=[CHANGE_THIS]/AppFlowy/appflowy_flutter
Exec=[CHANGE_THIS]/AppFlowy/AppFlowy
Categories=Office
Type=Application
Terminal=false

View File

@ -7,17 +7,19 @@
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
struct _MyApplication
{
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
char **dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
static void my_application_activate(GApplication *application)
{
MyApplication *self = MY_APPLICATION(application);
GtkWindow *window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) {
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
GdkScreen *screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen))
{
const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0)
{
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
if (use_header_bar)
{
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "appflowy_flutter");
gtk_header_bar_set_title(header_bar, "AppFlowy");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "appflowy_flutter");
}
else
{
gtk_window_set_title(window, "AppFlowy");
}
gtk_window_set_default_size(window, 1280, 720);
@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) {
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
FlView *view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) {
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
{
MyApplication *self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
if (!g_application_register(application, nullptr, &error))
{
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
@ -82,21 +91,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
static void my_application_dispose(GObject *object)
{
MyApplication *self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
static void my_application_class_init(MyApplicationClass *klass)
{
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
static void my_application_init(MyApplication *self) {}
MyApplication* my_application_new() {
MyApplication *my_application_new()
{
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,

View File

@ -5,10 +5,10 @@
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = appflowy_flutter
PRODUCT_NAME = AppFlowy
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
PRODUCT_COPYRIGHT = Copyright © 2023 AppFlowy.IO. All rights reserved.

View File

@ -365,7 +365,7 @@ ThemeData customizeEditorTheme(BuildContext context) {
return Theme.of(context).copyWith(extensions: [
editorStyle,
...darkPlguinStyleExtension,
...darkPluginStyleExtension,
quote,
]);
}

View File

@ -77,10 +77,10 @@ Next we will simulate the input of a shortcut key being pressed that will select
```dart
// Meta + A.
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
await editor.pressLogicKey(key: LogicalKeyboardKey.keyA, isMetaPressed: true);
// Meta + shift + S.
await editor.pressLogicKey(
LogicalKeyboardKey.keyS,
await editor.pressLogicKey
key: LogicalKeyboardKey.keyS,
isMetaPressed: true,
isShiftPressed: true,
);
@ -130,7 +130,7 @@ void main() async {
editor.insertTextNode(text);
}
await editor.startTesting();
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
await editor.pressLogicKey(key: LogicalKeyboardKey.keyA, isMetaPressed: true);
expect(
editor.documentSelection,

View File

@ -48,7 +48,7 @@ class _HomePageState extends State<HomePage> {
ThemeData _themeData = ThemeData.light().copyWith(
extensions: [
...lightEditorStyleExtension,
...lightPlguinStyleExtension,
...lightPluginStyleExtension,
],
);
@ -151,7 +151,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
// Theme Demo
_buildSeparator(context, 'Theme Demo'),
_buildListTile(context, 'Bulit In Dark Mode', () {
_buildListTile(context, 'Built In Dark Mode', () {
_jsonString = Future<String>.value(
jsonEncode(_editorState.document.toJson()).toString(),
);
@ -159,7 +159,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
_themeData = ThemeData.dark().copyWith(
extensions: [
...darkEditorStyleExtension,
...darkPlguinStyleExtension,
...darkPluginStyleExtension,
],
);
});
@ -372,7 +372,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
return Theme.of(context).copyWith(extensions: [
editorStyle,
...darkPlguinStyleExtension,
...darkPluginStyleExtension,
quote,
]);
}

View File

@ -34,7 +34,7 @@ ThemeData customizeEditorTheme(BuildContext context) {
return Theme.of(context).copyWith(extensions: [
editorStyle,
...darkPlguinStyleExtension,
...darkPluginStyleExtension,
quote,
]);
}

View File

@ -31,6 +31,7 @@ export 'src/extensions/attributes_extension.dart';
export 'src/render/rich_text/default_selectable.dart';
export 'src/render/rich_text/flowy_rich_text.dart';
export 'src/render/selection_menu/selection_menu_widget.dart';
export 'src/render/selection_menu/selection_menu_item_widget.dart';
export 'src/l10n/l10n.dart';
export 'src/render/style/plugin_styles.dart';
export 'src/render/style/editor_style.dart';

View File

@ -52,7 +52,7 @@ extension CommandExtension on EditorState {
throw Exception('path and textNode cannot be null at the same time');
}
String getTextInSelection(
List<String> getTextInSelection(
List<TextNode> textNodes,
Selection selection,
) {
@ -77,6 +77,6 @@ extension CommandExtension on EditorState {
}
}
}
return res.join('\n');
return res;
}
}

View File

@ -264,11 +264,11 @@ extension TextTransaction on Transaction {
if (index != 0 && attributes == null) {
newAttributes =
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
if (newAttributes != null) {
newAttributes = {...newAttributes}; // make a copy
} else {
newAttributes =
textNode.delta.slice(index, index + length).first.attributes;
if (newAttributes == null) {
final slicedDelta = textNode.delta.slice(index, index + length);
if (slicedDelta.isNotEmpty) {
newAttributes = slicedDelta.first.attributes;
}
}
}
updateText(
@ -276,7 +276,7 @@ extension TextTransaction on Transaction {
Delta()
..retain(index)
..delete(length)
..insert(text, attributes: newAttributes),
..insert(text, attributes: {...newAttributes ?? {}}),
);
afterSelection = Selection.collapsed(
Position(
@ -347,23 +347,34 @@ extension TextTransaction on Transaction {
textNode.toPlainText().length,
texts.first,
);
} else if (i == length - 1) {
} else if (i == length - 1 && texts.length >= 2) {
replaceText(
textNode,
0,
selection.endIndex,
texts.last,
);
} else if (i < texts.length - 1) {
replaceText(
textNode,
0,
textNode.toPlainText().length,
texts[i],
);
} else {
if (i < texts.length - 1) {
deleteNode(textNode);
if (i == textNodes.length - 1) {
final delta = Delta()
..insert(texts[0])
..addAll(
textNodes.last.delta.slice(selection.end.offset),
);
replaceText(
textNode,
0,
textNode.toPlainText().length,
texts[i],
selection.start.offset,
texts[0].length,
delta.toPlainText(),
);
} else {
deleteNode(textNode);
}
}
}
@ -373,6 +384,8 @@ extension TextTransaction on Transaction {
if (textNodes.length < texts.length) {
final length = texts.length;
var path = textNodes.first.path;
for (var i = 0; i < texts.length; i++) {
final text = texts[i];
if (i == 0) {
@ -382,13 +395,15 @@ extension TextTransaction on Transaction {
textNodes.first.toPlainText().length,
text,
);
} else if (i == length - 1) {
path = path.next;
} else if (i == length - 1 && textNodes.length >= 2) {
replaceText(
textNodes.last,
0,
selection.endIndex,
text,
);
path = path.next;
} else {
if (i < textNodes.length - 1) {
replaceText(
@ -397,14 +412,28 @@ extension TextTransaction on Transaction {
textNodes[i].toPlainText().length,
text,
);
path = path.next;
} else {
var path = textNodes.first.path;
var j = i - textNodes.length + length - 1;
while (j > 0) {
path = path.next;
j--;
if (i == texts.length - 1) {
final delta = Delta()
..insert(text)
..addAll(
textNodes.last.delta.slice(selection.end.offset),
);
insertNode(
path,
TextNode(
delta: delta,
),
);
} else {
insertNode(
path,
TextNode(
delta: Delta()..insert(text),
),
);
}
insertNode(path, TextNode(delta: Delta()..insert(text)));
}
}
}

View File

@ -77,7 +77,7 @@ class EditorState {
// TODO: only for testing.
bool disableSealTimer = false;
bool disbaleRules = false;
bool disableRules = false;
bool editable = true;
@ -209,7 +209,7 @@ class EditorState {
void _applyRules(int ruleCount) {
// Set a maximum count to prevent a dead loop.
if (ruleCount >= 5 || disbaleRules) {
if (ruleCount >= 5 || disableRules) {
return;
}

View File

@ -69,7 +69,7 @@ class SelectionMenu implements SelectionMenuService {
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorHeight = editorState.renderBox!.size.height;
// show below defualt
// show below default
var showBelow = true;
_alignment = Alignment.bottomLeft;
final bottomRight = selectionRects.first.bottomRight;
@ -91,20 +91,24 @@ class SelectionMenu implements SelectionMenuService {
top: showBelow ? _offset.dy : null,
bottom: showBelow ? null : _offset.dy,
left: offset.dx,
child: SelectionMenuWidget(
items: [
..._defaultSelectionMenuItems,
...editorState.selectionMenuItems,
],
maxItemInRow: 5,
editorState: editorState,
menuService: this,
onExit: () {
dismiss();
},
onSelectionUpdate: () {
_selectionUpdateByInner = true;
},
right: 0,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SelectionMenuWidget(
items: [
..._defaultSelectionMenuItems,
...editorState.selectionMenuItems,
],
maxItemInRow: 5,
editorState: editorState,
menuService: this,
onExit: () {
dismiss();
},
onSelectionUpdate: () {
_selectionUpdateByInner = true;
},
),
),
);
});

View File

@ -1,7 +1,6 @@
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -324,7 +323,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
_deleteLastCharacters();
return KeyEventResult.handled;
} else if (event.character != null &&
!arrowKeys.contains(event.logicalKey)) {
!arrowKeys.contains(event.logicalKey) && event.logicalKey != LogicalKeyboardKey.tab) {
keyword += event.character!;
_insertText(event.character!);
return KeyEventResult.handled;
@ -339,7 +338,14 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
newSelectedIndex -= 1;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
newSelectedIndex += 1;
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
newSelectedIndex += widget.maxItemInRow;
var currRow = (newSelectedIndex) % widget.maxItemInRow;
if (newSelectedIndex >= _showingItems.length) {
newSelectedIndex = (currRow + 1) % widget.maxItemInRow;
}
}
if (newSelectedIndex != _selectedIndex) {
setState(() {
_selectedIndex = newSelectedIndex.clamp(0, _showingItems.length - 1);

View File

@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
Iterable<ThemeExtension<dynamic>> get lightPlguinStyleExtension => [
Iterable<ThemeExtension<dynamic>> get lightPluginStyleExtension => [
HeadingPluginStyle.light,
CheckboxPluginStyle.light,
NumberListPluginStyle.light,
QuotedTextPluginStyle.light,
];
Iterable<ThemeExtension<dynamic>> get darkPlguinStyleExtension => [
Iterable<ThemeExtension<dynamic>> get darkPluginStyleExtension => [
HeadingPluginStyle.dark,
CheckboxPluginStyle.dark,
NumberListPluginStyle.dark,

View File

@ -16,14 +16,14 @@ class ToolbarWidget extends StatefulWidget {
required this.layerLink,
required this.offset,
required this.items,
this.aligment = Alignment.topLeft,
this.alignment = Alignment.topLeft,
}) : super(key: key);
final EditorState editorState;
final LayerLink layerLink;
final Offset offset;
final List<ToolbarItem> items;
final Alignment aligment;
final Alignment alignment;
@override
State<ToolbarWidget> createState() => _ToolbarWidgetState();
@ -41,7 +41,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
link: widget.layerLink,
showWhenUnlinked: true,
offset: widget.offset,
followerAnchor: widget.aligment,
followerAnchor: widget.alignment,
child: _buildToolbar(context),
),
);

View File

@ -39,7 +39,7 @@ class AppFlowyEditor extends StatefulWidget {
this.themeData = themeData ??
ThemeData.light().copyWith(extensions: [
...lightEditorStyleExtension,
...lightPlguinStyleExtension,
...lightPluginStyleExtension,
]);
}

View File

@ -232,7 +232,7 @@ void _pasteSingleLine(
/// parse url from the line text
/// reference: https://stackoverflow.com/questions/59444837/flutter-dart-regex-to-extract-urls-from-a-string
Delta _lineContentToDelta(String lineContent) {
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\#\w/\-?=%.]+');
final Iterable<RegExpMatch> matches = exp.allMatches(lineContent);
final delta = Delta();

View File

@ -310,7 +310,7 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) {
return KeyEventResult.handled;
};
ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) {
ShortcutEventHandler doubleAsteriskToBoldHandler = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
@ -366,8 +366,8 @@ ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) {
return KeyEventResult.handled;
};
//Implement in the same way as doubleAsteriskToBoldHanlder
ShortcutEventHandler doubleUnderscoreToBoldHanlder = (editorState, event) {
//Implement in the same way as doubleAsteriskToBoldHandler
ShortcutEventHandler doubleUnderscoreToBoldHandler = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();

View File

@ -8,7 +8,9 @@ ShortcutEventHandler selectAllHandler = (editorState, event) {
if (editorState.document.root.children.isEmpty) {
return KeyEventResult.handled;
}
final firstNode = editorState.document.root.children.first;
final firstNode = editorState.document.root.children.firstWhere(
(element) => element is TextNode,
);
final lastNode = editorState.document.root.children.last;
var offset = 0;
if (lastNode is TextNode) {

View File

@ -121,15 +121,13 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
return KeyEventResult.ignored;
}
Log.keyboard.debug('on keyboard event $event');
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
// TODO: use cache to optimize the searching time.
for (final shortcutEvent in widget.shortcutEvents) {
if (shortcutEvent.keybindings.containsKeyEvent(event)) {
if (shortcutEvent.canRespondToRawKeyEvent(event)) {
final result = shortcutEvent.handler(widget.editorState, event);
if (result == KeyEventResult.handled) {
return KeyEventResult.handled;
@ -157,3 +155,10 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
return onKey(event);
}
}
extension on ShortcutEvent {
bool canRespondToRawKeyEvent(RawKeyEvent event) {
return ((character?.isNotEmpty ?? false) && character == event.character) ||
keybindings.containsKeyEvent(event);
}
}

View File

@ -82,6 +82,13 @@ abstract class AppFlowySelectionService {
/// The current selection areas's rect in editor.
List<Rect> get selectionRects;
void register(SelectionInterceptor interceptor);
void unRegister(SelectionInterceptor interceptor);
}
class SelectionInterceptor {
bool Function(TapDownDetails details)? canTap;
}
class AppFlowySelection extends StatefulWidget {
@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
selectionRects.clear();
clearSelection();
_clearToolbar();
if (selection != null) {
if (selection.isCollapsed) {
@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
}
void _onTapDown(TapDownDetails details) {
final canTap =
_interceptors.every((element) => element.canTap?.call(details) ?? true);
if (!canTap) return;
// clear old state.
_panStartOffset = null;
@ -701,4 +713,15 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
// }
// }
}
final List<SelectionInterceptor> _interceptors = [];
@override
void register(SelectionInterceptor interceptor) {
_interceptors.add(interceptor);
}
@override
void unRegister(SelectionInterceptor interceptor) {
_interceptors.removeWhere((element) => element == interceptor);
}
}

View File

@ -247,7 +247,7 @@ List<ShortcutEvent> builtInShortcutEvents = [
),
ShortcutEvent(
key: 'selection menu',
command: 'slash,shift+slash',
character: '/',
handler: slashShortcutHandler,
),
ShortcutEvent(
@ -289,7 +289,7 @@ List<ShortcutEvent> builtInShortcutEvents = [
),
ShortcutEvent(
key: 'Double tilde to strikethrough',
command: 'tilde,shift+tilde',
character: '~',
handler: doubleTildeToStrikethrough,
),
ShortcutEvent(
@ -304,18 +304,18 @@ List<ShortcutEvent> builtInShortcutEvents = [
),
ShortcutEvent(
key: 'Underscore to italic',
command: 'shift+underscore',
character: '_',
handler: underscoreToItalicHandler,
),
ShortcutEvent(
key: 'Double asterisk to bold',
command: 'shift+digit 8',
handler: doubleAsteriskToBoldHanlder,
character: '*',
handler: doubleAsteriskToBoldHandler,
),
ShortcutEvent(
key: 'Double underscore to bold',
command: 'shift+underscore',
handler: doubleUnderscoreToBoldHanlder,
character: '_',
handler: doubleUnderscoreToBoldHandler,
),
// https://github.com/flutter/flutter/issues/104944
// Workaround: Using space editing on the web platform often results in errors,

View File

@ -8,12 +8,29 @@ import 'package:flutter/foundation.dart';
class ShortcutEvent {
ShortcutEvent({
required this.key,
required this.command,
this.character,
this.command,
required this.handler,
String? windowsCommand,
String? macOSCommand,
String? linuxCommand,
}) {
// character and command cannot be null at the same time
assert(
!(character == null &&
command == null &&
windowsCommand == null &&
macOSCommand == null &&
linuxCommand == null),
'character and command cannot be null at the same time');
assert(
!(character != null &&
(command != null &&
windowsCommand != null &&
macOSCommand != null &&
linuxCommand != null)),
'character and command cannot be set at the same time');
updateCommand(
command: command,
windowsCommand: windowsCommand,
@ -43,7 +60,9 @@ class ShortcutEvent {
///
/// Like, 'ctrl+c,cmd+c'
///
String command;
String? command;
String? character;
final ShortcutEventHandler handler;
@ -80,9 +99,9 @@ class ShortcutEvent {
matched = true;
}
if (matched) {
if (matched && this.command != null) {
_keybindings = this
.command
.command!
.split(',')
.map((e) => Keybinding.parse(e))
.toList(growable: false);

View File

@ -66,7 +66,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
layerLink: layerLink,
offset: offset,
items: items,
aligment: alignment,
alignment: alignment,
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);

View File

@ -4,7 +4,7 @@ import '../infra/test_editor.dart';
void main() {
group('command_extension.dart', () {
testWidgets('insert a new checkbox after an exsiting checkbox',
testWidgets('insert a new checkbox after an existing checkbox',
(tester) async {
final editor = tester.editor
..insertTextNode(
@ -26,11 +26,11 @@ void main() {
.editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>()
.toList(growable: false);
final text = editor.editorState.getTextInSelection(
final texts = editor.editorState.getTextInSelection(
textNodes.normalized,
selection.normalized,
);
expect(text, 'me\nto\nAppfl');
expect(texts, ['me', 'to', 'Appfl']);
});
});
}

View File

@ -91,6 +91,43 @@ void main() async {
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length >> texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 5);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [4], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2, 3, 4]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 1);
textNodes = [0]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC456789');
});
testWidgets('test replaceTexts, textNodes.length < texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
@ -128,5 +165,42 @@ void main() async {
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length << texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor..insertTextNode('Welcome to AppFlowy!');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 1);
// select 'to'
final selection = Selection(
start: Position(path: [0], offset: 8),
end: Position(path: [0], offset: 10),
);
final transaction = editor.editorState.transaction;
var textNodes = [0]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC1', 'ABC2', 'ABC3', 'ABC4', 'ABC5'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 5);
textNodes = [0, 1, 2, 3, 4]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), 'Welcome ABC1');
expect(textNodes[1].toPlainText(), 'ABC2');
expect(textNodes[2].toPlainText(), 'ABC3');
expect(textNodes[3].toPlainText(), 'ABC4');
expect(textNodes[4].toPlainText(), 'ABC5 AppFlowy!');
});
});
}

Some files were not shown because too many files have changed in this diff Show More