Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966547faa0 | ||
|
|
3647af44c9 | ||
|
|
25b4a647b0 | ||
|
|
c6e3c1fd7c | ||
|
|
4fd35d0de4 | ||
|
|
7406c5e6a5 | ||
|
|
aa70885b21 | ||
|
|
8699c9c125 | ||
|
|
bc758e0b06 | ||
|
|
e38980ccc3 | ||
|
|
8dd180046f | ||
|
|
0cd2726752 | ||
|
|
11daa15b89 | ||
|
|
8436bc6144 | ||
|
|
59851f46b7 | ||
|
|
62f1d6eb82 | ||
|
|
ec80f72c8f | ||
|
|
b2c80981cc | ||
|
|
8fa425a76c | ||
|
|
ebe112581d | ||
|
|
bc8f35d7db | ||
|
|
058eeec932 | ||
|
|
8e10cba8e5 | ||
|
|
7a24f6b6f0 | ||
|
|
4f40945458 | ||
|
|
e59870e421 | ||
|
|
cf521fe20b | ||
|
|
f0d47ee50c | ||
|
|
2a5d393c9f | ||
|
|
98e0b832ec | ||
|
|
77e7cbe9ec | ||
|
|
acd53eb640 | ||
|
|
ffdf5d24a0 | ||
|
|
b247a49417 | ||
|
|
dace02d34d | ||
|
|
41d4351176 | ||
|
|
d4bc575c03 | ||
|
|
a59561aee3 | ||
|
|
92523321f1 | ||
|
|
537c9d0865 | ||
|
|
b4051c7e9f | ||
|
|
9e61133886 | ||
|
|
36f47f3636 | ||
|
|
40dcd13394 | ||
|
|
eb20c7c117 | ||
|
|
e9852a3934 | ||
|
|
78d8ec9a3e | ||
|
|
4220309cf8 | ||
|
|
812af0979e | ||
|
|
cca5005541 | ||
|
|
9306d4a4ee | ||
|
|
3ab1ff5e73 | ||
|
|
05fa306782 | ||
|
|
d054439df4 | ||
|
|
15aaacc4b7 | ||
|
|
ef9de52cbc |
No files matched your search
@@ -21,7 +21,7 @@ Before you mark this PR ready for review, run through this checklist!
|
||||
|
||||
#### PR Checklist
|
||||
|
||||
- [ ] My code adheres to the [AppFlowy Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
|
||||
- [ ] My code adheres to [AppFlowy's Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions)
|
||||
- [ ] I've listed at least one issue that this PR fixes in the description above.
|
||||
- [ ] I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
|
||||
- [ ] All existing tests are passing.
|
||||
@@ -49,6 +49,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
@@ -45,6 +45,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
df -h
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ jobs:
|
||||
echo SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} >> .env.ci
|
||||
echo SUPABASE_JWT_SECRET=${{ secrets.SUPABASE_JWT_SECRET }} >> .env.ci
|
||||
|
||||
- name: Create .env file in flowy-test
|
||||
working-directory: frontend/rust-lib/flowy-test
|
||||
- name: Create .env file in event-integration
|
||||
working-directory: frontend/rust-lib/event-integration
|
||||
run: |
|
||||
touch .env.ci
|
||||
echo SUPABASE_URL=${{ secrets.SUPABASE_URL }} >> .env.ci
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.3.6 - 10/16/2023
|
||||
|
||||
### New Features
|
||||
- Support setting Markdown styles through keyboard shortcuts.
|
||||
- Added Ukrainian language.
|
||||
- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window.
|
||||
- Support toggling the notifitcation on/off.
|
||||
- Added Lemonade theme.
|
||||
|
||||
|
||||
### Bug fixes
|
||||
- Improve Vietnamese translations.
|
||||
- Improve reminder feature.
|
||||
- Fix some UI issues.
|
||||
|
||||
## Version 0.3.5 - 10/09/2023
|
||||
|
||||
### New Features
|
||||
- Added support for browsing and inserting images from Unsplash.
|
||||
- Revamp and unify the emoji picker throughout AppFlowy.
|
||||
|
||||
### Bug fixes
|
||||
- Improve layout of the settings page.
|
||||
- Improve design of the restore page banner.
|
||||
- Improve UX of the reminders.
|
||||
- Other UI fixes.
|
||||
|
||||
## Version 0.3.4 - 10/02/2023
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -47,7 +47,7 @@ You are in charge of your data and customizations.
|
||||
|
||||
## Getting Started with development
|
||||
|
||||
Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -73,7 +73,7 @@ Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitt
|
||||
|
||||
[](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
|
||||
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/appflowy_flutter/assets/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
|
||||
|
||||
## Join the community to build AppFlowy together
|
||||
|
||||
Vendored
+8
@@ -285,6 +285,14 @@
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate AppFlowyEnv",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build --delete-conflicting-outputs",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,7 +25,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.4"
|
||||
CURRENT_APP_VERSION = "0.3.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -13,11 +13,13 @@ This Repository contains the codebase for the frontend of the application, curre
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
> We later expect to extend support to Android and iOS devices using Flutter.
|
||||
> We are actively working on support for Android & iOS!
|
||||
|
||||
_Additionally, we are working on a Web version built with Tauri!_
|
||||
|
||||
### Am I Eligible to Contribute?
|
||||
|
||||
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are:
|
||||
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy). Some of the ways in which you can contribute are:
|
||||
|
||||
- Non-Coding Contributions
|
||||
- Documentation
|
||||
@@ -26,27 +28,23 @@ Yes! You are eligible to contribute, check out the ways in which you can [contri
|
||||
- Improve Translations
|
||||
- Coding Contributions
|
||||
|
||||
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab).
|
||||
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we suggest you learn the basics, and then contribute afterwards. To get started with Flutter read [here](https://flutter.dev/docs/get-started/codelab).
|
||||
|
||||
### What OS Should I Use for Development?
|
||||
### What OS should I use for development?
|
||||
|
||||
We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS.
|
||||
We support all OS for Development i.e. Linux, MacOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://docs.appflowy.io/docs/documentation/appflowy/from-source/environment-setup) on how to setup `AppFlowy_Flutter` on your local system respectively per operating system.
|
||||
|
||||
### Getting Started ❇
|
||||
|
||||
We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you.
|
||||
We have detailed documentation on how to [get started](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you:
|
||||
|
||||
- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap)
|
||||
- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request)
|
||||
- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
|
||||
- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks)
|
||||
- [Styleguide & Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions/naming-conventions)
|
||||
- [Making Your First PR](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/submitting-your-first-pull-request)
|
||||
- [All AppFlowy Documentation](https://docs.appflowy.io/docs/documentation/appflowy) - Contribution guide, build and run, debugging, testing, localization, etc.
|
||||
|
||||
### Need Help?
|
||||
|
||||
- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
|
||||
- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you!
|
||||
|
||||
<!--
|
||||
## release check
|
||||
1. [entitlements](https://flutter.dev/desktop#setting-up-entitlements)
|
||||
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop) -->
|
||||
- New to GitHub? Follow [these](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
|
||||
- Stuck Somewhere? Join our [Discord](https://discord.gg/9Q2xaN37tV), we're there to help you!
|
||||
- Find out more about the [community initiatives](https://docs.appflowy.io/docs/appflowy/community).
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
# Configuring Cloud Type
|
||||
# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are:
|
||||
# Supabase: Set CLOUD_TYPE to 1
|
||||
# AppFlowy Cloud: Set CLOUD_TYPE to 2
|
||||
# Local: 0
|
||||
# Supabase: 1
|
||||
# AppFlowy Cloud: 2
|
||||
|
||||
CLOUD_TYPE=1
|
||||
CLOUD_TYPE=0
|
||||
|
||||
# Supabase Configuration
|
||||
# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
|
||||
@@ -18,4 +19,5 @@ SUPABASE_ANON_KEY=replace-with-your-supabase-key
|
||||
# AppFlowy Cloud Configuration
|
||||
# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations:
|
||||
APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url
|
||||
APPFLOWY_CLOUD_BASE_WS_URL=replace-with-your-appflowy-cloud-ws-url
|
||||
APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url
|
||||
APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url
|
||||
@@ -245,7 +245,7 @@ void main() {
|
||||
await tester.wait(500);
|
||||
|
||||
// Focus on the editor
|
||||
final textBlock = find.byType(TextBlockComponentWidget);
|
||||
final textBlock = find.byType(ParagraphBlockComponentWidget);
|
||||
await tester.tapAt(tester.getCenter(textBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ void main() {
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('delete the readme page and restore it', (tester) async {
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'document_option_action_test.dart' as document_option_action_test;
|
||||
import 'document_text_direction_test.dart' as document_text_direction_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
@@ -33,4 +34,5 @@ void startTesting() {
|
||||
document_alignment_test.main();
|
||||
document_text_direction_test.main();
|
||||
document_option_action_test.main();
|
||||
document_with_image_block_test.main();
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:run_with_network_images/run_with_network_images.dart';
|
||||
|
||||
import '../util/mock/mock_file_picker.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('image block in document', () {
|
||||
testWidgets('insert an image from local file', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(
|
||||
paths: [imagePath],
|
||||
);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
|
||||
// remove the temp file
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('insert an image from network', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
const url =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], url);
|
||||
});
|
||||
|
||||
testWidgets('insert an image from unsplash', (tester) async {
|
||||
await runWithNetworkImages(() async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
'Unsplash',
|
||||
);
|
||||
expect(find.byType(UnsplashImageWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -78,7 +79,7 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
const expectedMarkdown = r'''
|
||||
const expectedMarkdown = '''
|
||||
# Welcome to AppFlowy!
|
||||
## Here are the basics
|
||||
- [ ] Click anywhere and just start typing.
|
||||
@@ -105,6 +106,15 @@ fn main() {
|
||||
## Have a question❓
|
||||
> Click `?` at the bottom right for help and support.
|
||||
|
||||
> 🥰
|
||||
>
|
||||
> Like AppFlowy? Follow us:
|
||||
> [GitHub](https://github.com/AppFlowy-IO/AppFlowy)
|
||||
> [Twitter](https://twitter.com/appflowy): @appflowy
|
||||
> [Newsletter](https://blog-appflowy.ghost.io/)
|
||||
>
|
||||
|
||||
|
||||
|
||||
|
||||
''';
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('create a new document, grid, board and calendar',
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
break;
|
||||
case ViewLayoutPB.Grid:
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -11,6 +14,10 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('customize the folder path', () {
|
||||
if (Platform.isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const userA = 'UserA';
|
||||
const userB = 'UserB';
|
||||
|
||||
@@ -54,7 +54,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
|
||||
+28
-3
@@ -35,10 +35,17 @@ abstract class Env {
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_BASE_WS_URL',
|
||||
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudBaseWSUrl = _Env.afCloudBaseWSUrl;
|
||||
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
|
||||
|
||||
// Supabase Configuration:
|
||||
@EnviedField(
|
||||
@@ -64,6 +71,24 @@ bool get isCloudEnabled {
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType() == CloudType.supabase;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
// Only enable appflowy cloud in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType() == CloudType.appflowyCloud;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum CloudType {
|
||||
unknown,
|
||||
supabase,
|
||||
@@ -84,7 +109,7 @@ CloudType currentCloudType() {
|
||||
}
|
||||
|
||||
if (value == 2) {
|
||||
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudBaseWSUrl.isEmpty) {
|
||||
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) {
|
||||
Log.error("AppFlowy cloud is not configured");
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
|
||||
+4
-8
@@ -381,14 +381,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
|
||||
switch (customData.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
if (group.isCheck) {
|
||||
widget = const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
} else {
|
||||
widget = const FlowySvg(FlowySvgs.uncheck_s);
|
||||
}
|
||||
widget = FlowySvg(
|
||||
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
break;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.medium(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
LocaleKeys.grid_field_numberFormat.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
+1
-1
@@ -230,7 +230,7 @@ class _RowMenuButtonState extends State<RowMenuButton> {
|
||||
onPressed: () => widget.openMenu(),
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
FlowySvgs.drag_element_s,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
);
|
||||
|
||||
+5
-6
@@ -39,12 +39,11 @@ class _CheckboxCellState extends State<CheckboxCardCell> {
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isSelected != current.isSelected,
|
||||
builder: (context, state) {
|
||||
final icon = state.isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
)
|
||||
: const FlowySvg(FlowySvgs.uncheck_s);
|
||||
final icon = FlowySvg(
|
||||
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -66,7 +67,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
|
||||
+4
-1
@@ -116,6 +116,9 @@ class CheckboxCellUncheck extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const FlowySvg(FlowySvgs.uncheck_s);
|
||||
return const FlowySvg(
|
||||
FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -86,15 +86,15 @@ class DateCellCalendarBloc
|
||||
: (start, state.startDay!);
|
||||
emit(state.copyWith(startDay: null, endDay: null));
|
||||
await _updateDateData(
|
||||
date: newStart.toLocal().date,
|
||||
endDate: newEnd.toLocal().date,
|
||||
date: newStart.date,
|
||||
endDate: newEnd.date,
|
||||
);
|
||||
} else if (end == null) {
|
||||
emit(state.copyWith(startDay: start, endDay: null));
|
||||
} else {
|
||||
await _updateDateData(
|
||||
date: start!.toLocal().date,
|
||||
endDate: end.toLocal().date,
|
||||
date: start!.date,
|
||||
endDate: end.date,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+9
-2
@@ -13,7 +13,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/time/prelude.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -282,7 +281,7 @@ class _DatePickerState extends State<DatePicker> {
|
||||
state.isRange ? false : isSameDay(state.dateTime, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
context.read<DateCellCalendarBloc>().add(
|
||||
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
||||
DateCellCalendarEvent.selectDay(selectedDay),
|
||||
);
|
||||
},
|
||||
onRangeSelected: (start, end, focusedDay) {
|
||||
@@ -367,6 +366,9 @@ class EndTimeButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
const _maxLengthTwelveHour = 8;
|
||||
const _maxLengthTwentyFourHour = 5;
|
||||
|
||||
class _TimeTextField extends StatefulWidget {
|
||||
final bool isEndTime;
|
||||
final String? timeStr;
|
||||
@@ -434,6 +436,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
errorText: widget.isEndTime
|
||||
? state.parseEndTimeError
|
||||
: state.parseTimeError,
|
||||
maxLength:
|
||||
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwelveHour
|
||||
? _maxLengthTwelveHour
|
||||
: _maxLengthTwentyFourHour,
|
||||
showCounter: false,
|
||||
onSubmitted: (timeStr) {
|
||||
if (widget.isEndTime) {
|
||||
context
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class GridTextCellStyle extends GridCellStyle {
|
||||
this.autofocus = false,
|
||||
this.showEmoji = true,
|
||||
this.emojiFontSize = 16,
|
||||
this.emojiHPadding = 0,
|
||||
this.emojiHPadding = 4,
|
||||
this.useRoundedBorder = false,
|
||||
});
|
||||
}
|
||||
|
||||
+5
-5
@@ -144,8 +144,10 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
|
||||
_controller.text = state.content;
|
||||
},
|
||||
builder: (context, state) {
|
||||
final style = widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium!;
|
||||
widget._cellDataNotifier.value = state.content;
|
||||
final urlEditor = Padding(
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
@@ -154,9 +156,7 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
style: (widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
style: style.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -168,11 +168,11 @@ class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
hintStyle: style.copyWith(color: Theme.of(context).hintColor),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
return urlEditor;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,8 +5,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_b
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@@ -112,6 +113,7 @@ class RowPropertyList extends StatelessWidget {
|
||||
class _PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
|
||||
final int index;
|
||||
const _PropertyCell({
|
||||
required this.cellContext,
|
||||
@@ -126,6 +128,8 @@ class _PropertyCell extends StatefulWidget {
|
||||
|
||||
class _PropertyCellState extends State<_PropertyCell> {
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
final PopoverController _fieldPopoverController = PopoverController();
|
||||
|
||||
bool _isFieldHover = false;
|
||||
|
||||
@override
|
||||
@@ -140,7 +144,23 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 30,
|
||||
child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
|
||||
child: AppFlowyPopover(
|
||||
controller: _fieldPopoverController,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
margin: EdgeInsets.zero,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: _isFieldHover
|
||||
? BlockActionButton(
|
||||
onTap: () => _fieldPopoverController.show(),
|
||||
svg: FlowySvgs.drag_element_s,
|
||||
richMessage: TextSpan(
|
||||
text: LocaleKeys.grid_rowPage_fieldDragEelementTooltip.tr(),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ class DatabaseShareActionListState extends State<DatabaseShareActionList> {
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ class DocumentBanner extends StatelessWidget {
|
||||
constraints: const BoxConstraints(minHeight: 60),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: colorScheme.error,
|
||||
color: colorScheme.surfaceVariant,
|
||||
child: FittedBox(
|
||||
alignment: Alignment.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
@@ -30,7 +30,7 @@ class DocumentBanner extends StatelessWidget {
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_text.tr(),
|
||||
color: colorScheme.onError,
|
||||
color: colorScheme.tertiary,
|
||||
fontSize: 14,
|
||||
),
|
||||
const HSpace(20),
|
||||
@@ -39,13 +39,13 @@ class DocumentBanner extends StatelessWidget {
|
||||
minHeight: 40,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: Colors.transparent,
|
||||
highlightColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
outlineColor: colorScheme.onError,
|
||||
highlightColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
outlineColor: colorScheme.tertiaryContainer,
|
||||
borderRadius: Corners.s8Border,
|
||||
onPressed: onRestore,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
||||
color: colorScheme.onError,
|
||||
color: colorScheme.tertiary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
@@ -55,13 +55,13 @@ class DocumentBanner extends StatelessWidget {
|
||||
minHeight: 40,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: Colors.transparent,
|
||||
highlightColor: Theme.of(context).colorScheme.primary,
|
||||
outlineColor: colorScheme.onError,
|
||||
highlightColor: Theme.of(context).colorScheme.error,
|
||||
outlineColor: colorScheme.tertiaryContainer,
|
||||
borderRadius: Corners.s8Border,
|
||||
onPressed: onDelete,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
color: colorScheme.onError,
|
||||
color: colorScheme.tertiary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
|
||||
@@ -6,8 +8,9 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -57,7 +60,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
late final InlineActionsService inlineActionsService = InlineActionsService(
|
||||
context: context,
|
||||
handlers: [
|
||||
InlinePageReferenceService().inlinePageReferenceDelegate,
|
||||
InlinePageReferenceService(
|
||||
currentViewId: documentBloc.view.id,
|
||||
).inlinePageReferenceDelegate,
|
||||
DateReferenceService(context).dateReferenceDelegate,
|
||||
ReminderReferenceService(context).reminderReferenceDelegate,
|
||||
],
|
||||
@@ -147,7 +152,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
|
||||
// keep the previous font style when typing new text.
|
||||
supportSlashMenuNodeWhiteList.addAll([
|
||||
ToggleListBlockKeys.type,
|
||||
]);
|
||||
toolbarItemWhiteList.addAll([
|
||||
ToggleListBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
TableBlockKeys.type,
|
||||
]);
|
||||
AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
|
||||
|
||||
// customize the dynamic theme color
|
||||
_customizeBlockComponentBackgroundColorDecorator();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -233,7 +249,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
final customBlockComponentBuilderMap = {
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
ParagraphBlockKeys.type: TextBlockComponentBuilder(
|
||||
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||
@@ -264,10 +280,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
),
|
||||
ImageBlockKeys.type: ImageBlockComponentBuilder(
|
||||
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (node, state) => Positioned(
|
||||
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 10,
|
||||
child: ImageMenu(
|
||||
@@ -348,6 +365,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
),
|
||||
),
|
||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
final builders = {
|
||||
@@ -523,4 +545,33 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _customizeBlockComponentBackgroundColorDecorator() {
|
||||
blockComponentBackgroundColorDecorator = (Node node, String colorString) {
|
||||
// the color string is from FlowyTint.
|
||||
final tintColor = FlowyTint.values.firstWhereOrNull(
|
||||
(e) => e.id == colorString,
|
||||
);
|
||||
if (tintColor != null) {
|
||||
return tintColor.color(context);
|
||||
}
|
||||
final themeColor = themeBackgroundColors[colorString];
|
||||
if (themeColor != null) {
|
||||
return themeColor.color(context);
|
||||
}
|
||||
|
||||
if (colorString == optionActionColorDefaultColor) {
|
||||
final defaultColor = node.type == CalloutBlockKeys.type
|
||||
? AFThemeExtension.of(context).calloutBGColor
|
||||
: Colors.transparent;
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
if (colorString == tableCellDefaultColor) {
|
||||
return AFThemeExtension.of(context).tableCellBGColor;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -20,7 +21,7 @@ class BlockActionButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
child: FlowyTooltip(
|
||||
preferBelow: false,
|
||||
richMessage: richMessage,
|
||||
child: MouseRegion(
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@@ -47,7 +47,7 @@ class BlockOptionButton extends StatelessWidget {
|
||||
: PopoverDirection.leftWithCenterAligned,
|
||||
actions: popoverActions,
|
||||
onPopupBuilder: () {
|
||||
keepEditorFocusNotifier.value += 1;
|
||||
keepEditorFocusNotifier.increase();
|
||||
blockComponentState.alwaysShowActions = true;
|
||||
},
|
||||
onClosed: () {
|
||||
@@ -55,7 +55,7 @@ class BlockOptionButton extends StatelessWidget {
|
||||
editorState.selectionType = null;
|
||||
editorState.selection = null;
|
||||
blockComponentState.alwaysShowActions = false;
|
||||
keepEditorFocusNotifier.value -= 1;
|
||||
keepEditorFocusNotifier.decrease();
|
||||
});
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
|
||||
+8
-5
@@ -10,6 +10,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
const optionActionColorDefaultColor = 'appflowy_theme_default_color';
|
||||
|
||||
enum OptionAction {
|
||||
delete,
|
||||
duplicate,
|
||||
@@ -251,12 +253,14 @@ class ColorOptionAction extends PopoverActionCell {
|
||||
// reset to default background color
|
||||
FlowyColorOption(
|
||||
color: defaultColor,
|
||||
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
|
||||
i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
|
||||
id: optionActionColorDefaultColor,
|
||||
),
|
||||
...FlowyTint.values.map(
|
||||
(e) => FlowyColorOption(
|
||||
color: e.color(context),
|
||||
name: e.tintName(AppFlowyEditorLocalizations.current),
|
||||
i18n: e.tintName(AppFlowyEditorLocalizations.current),
|
||||
id: e.id,
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -268,11 +272,10 @@ class ColorOptionAction extends PopoverActionCell {
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
width: 1,
|
||||
),
|
||||
onTap: (color, index) async {
|
||||
onTap: (option, index) async {
|
||||
final transaction = editorState.transaction;
|
||||
final backgroundColor = color.toHex();
|
||||
transaction.updateNode(node, {
|
||||
blockComponentBackgroundColor: backgroundColor,
|
||||
blockComponentBackgroundColor: option.id,
|
||||
});
|
||||
await editorState.apply(transaction);
|
||||
|
||||
|
||||
-27
@@ -3,7 +3,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/op
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -112,32 +111,6 @@ class OptionActionList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class BlockComponentActionButton extends StatelessWidget {
|
||||
const BlockComponentActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isHovering = false;
|
||||
final Widget icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onTapDown: (details) {},
|
||||
onTapUp: (details) {},
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OptionActionButton extends StatelessWidget {
|
||||
const OptionActionButton({
|
||||
super.key,
|
||||
|
||||
+5
-4
@@ -4,13 +4,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final alignToolbarItem = ToolbarItem(
|
||||
id: 'editor.align',
|
||||
group: 4,
|
||||
isActive: onlyShowInTextType,
|
||||
builder: (context, editorState, highlightColor) {
|
||||
builder: (context, editorState, highlightColor, _) {
|
||||
final selection = editorState.selection!;
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
|
||||
@@ -88,11 +89,11 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
popupBuilder: (_) {
|
||||
keepEditorFocusNotifier.value += 1;
|
||||
keepEditorFocusNotifier.increase();
|
||||
return _AlignButtons(onAlignChanged: widget.onAlignChanged);
|
||||
},
|
||||
onClose: () {
|
||||
keepEditorFocusNotifier.value -= 1;
|
||||
keepEditorFocusNotifier.decrease();
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
@@ -155,7 +156,7 @@ class _AlignButton extends StatelessWidget {
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Tooltip(
|
||||
child: FlowyTooltip(
|
||||
message: tooltips,
|
||||
child: FlowySvg(
|
||||
icon,
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
// DON'T MODIFY THIS KEY BECAUSE IT'S SAVED IN THE DATABASE!
|
||||
// Used for the block component background color
|
||||
const themeBackgroundColors = {
|
||||
'appflowy_them_color_tint1': FlowyTint.tint1,
|
||||
'appflowy_them_color_tint2': FlowyTint.tint2,
|
||||
'appflowy_them_color_tint3': FlowyTint.tint3,
|
||||
'appflowy_them_color_tint4': FlowyTint.tint4,
|
||||
'appflowy_them_color_tint5': FlowyTint.tint5,
|
||||
'appflowy_them_color_tint6': FlowyTint.tint6,
|
||||
'appflowy_them_color_tint7': FlowyTint.tint7,
|
||||
'appflowy_them_color_tint8': FlowyTint.tint8,
|
||||
'appflowy_them_color_tint9': FlowyTint.tint9,
|
||||
};
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
+6
-8
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
@@ -6,13 +7,12 @@ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.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/error_page.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';
|
||||
|
||||
void showLinkToPageMenu(
|
||||
OverlayState container,
|
||||
@@ -27,13 +27,13 @@ void showLinkToPageMenu(
|
||||
final top = alignment == Alignment.bottomLeft ? offset.dy : null;
|
||||
final bottom = alignment == Alignment.topLeft ? offset.dy : null;
|
||||
|
||||
keepEditorFocusNotifier.value += 1;
|
||||
keepEditorFocusNotifier.increase();
|
||||
late OverlayEntry linkToPageMenuEntry;
|
||||
linkToPageMenuEntry = FullScreenOverlayEntry(
|
||||
top: top,
|
||||
bottom: bottom,
|
||||
left: offset.dx,
|
||||
dismissCallback: () => keepEditorFocusNotifier.value -= 1,
|
||||
dismissCallback: () => keepEditorFocusNotifier.decrease(),
|
||||
builder: (context) => Material(
|
||||
color: Colors.transparent,
|
||||
child: LinkToPageMenu(
|
||||
@@ -225,11 +225,9 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
future: items,
|
||||
);
|
||||
|
||||
+9
-7
@@ -88,8 +88,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
||||
bool validate(Node node) =>
|
||||
node.delta != null &&
|
||||
node.children.isEmpty &&
|
||||
node.attributes[CalloutBlockKeys.icon] is String &&
|
||||
node.attributes[CalloutBlockKeys.backgroundColor] is String;
|
||||
node.attributes[CalloutBlockKeys.icon] is String;
|
||||
}
|
||||
|
||||
// the main widget for rendering the callout block
|
||||
@@ -117,7 +116,8 @@ class _CalloutBlockComponentWidgetState
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BlockComponentTextDirectionMixin,
|
||||
BlockComponentAlignMixin {
|
||||
BlockComponentAlignMixin,
|
||||
BlockComponentBackgroundColorMixin {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
|
||||
@@ -137,11 +137,13 @@ class _CalloutBlockComponentWidgetState
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
// get the background color of the note block from the node's attributes
|
||||
@override
|
||||
Color get backgroundColor {
|
||||
final colorString =
|
||||
node.attributes[CalloutBlockKeys.backgroundColor] as String;
|
||||
return colorString.tryToColor() ?? Colors.transparent;
|
||||
final color = super.backgroundColor;
|
||||
if (color == Colors.transparent) {
|
||||
return AFThemeExtension.of(context).calloutBGColor;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// get the emoji of the note block from the node's attributes or default to '📌'
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart' as highlight;
|
||||
@@ -190,7 +191,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
Widget child = Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
color: AFThemeExtension.of(context).calloutBGColor,
|
||||
),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export 'src/config.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
||||
export 'src/models/emoji_model.dart';
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'models/category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
/// Config for customizations
|
||||
class Config {
|
||||
/// Constructor
|
||||
const Config({
|
||||
this.columns = 7,
|
||||
this.emojiSizeMax = 32.0,
|
||||
this.verticalSpacing = 0,
|
||||
this.horizontalSpacing = 0,
|
||||
this.initCategory = Category.RECENT,
|
||||
this.bgColor = const Color(0xFFEBEFF2),
|
||||
this.indicatorColor = Colors.blue,
|
||||
this.iconColor = Colors.grey,
|
||||
this.iconColorSelected = Colors.blue,
|
||||
this.progressIndicatorColor = Colors.blue,
|
||||
this.backspaceColor = Colors.blue,
|
||||
this.showRecentsTab = true,
|
||||
this.recentsLimit = 28,
|
||||
this.noRecentsText = 'No Recents',
|
||||
this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26),
|
||||
this.tabIndicatorAnimDuration = kTabScrollDuration,
|
||||
this.categoryIcons = const CategoryIcons(),
|
||||
this.buttonMode = ButtonMode.MATERIAL,
|
||||
});
|
||||
|
||||
/// Number of emojis per row
|
||||
final int columns;
|
||||
|
||||
/// Width and height the emoji will be maximal displayed
|
||||
/// Can be smaller due to screen size and amount of columns
|
||||
final double emojiSizeMax;
|
||||
|
||||
/// Vertical spacing between emojis
|
||||
final double verticalSpacing;
|
||||
|
||||
/// Horizontal spacing between emojis
|
||||
final double horizontalSpacing;
|
||||
|
||||
/// The initial [Category] that will be selected
|
||||
/// This [Category] will have its button in the bottombar darkened
|
||||
final Category initCategory;
|
||||
|
||||
/// The background color of the Widget
|
||||
final Color bgColor;
|
||||
|
||||
/// The color of the category indicator
|
||||
final Color indicatorColor;
|
||||
|
||||
/// The color of the category icons
|
||||
final Color iconColor;
|
||||
|
||||
/// The color of the category icon when selected
|
||||
final Color iconColorSelected;
|
||||
|
||||
/// The color of the loading indicator during initialization
|
||||
final Color progressIndicatorColor;
|
||||
|
||||
/// The color of the backspace icon button
|
||||
final Color backspaceColor;
|
||||
|
||||
/// Show extra tab with recently used emoji
|
||||
final bool showRecentsTab;
|
||||
|
||||
/// Limit of recently used emoji that will be saved
|
||||
final int recentsLimit;
|
||||
|
||||
/// The text to be displayed if no recent emojis to display
|
||||
final String noRecentsText;
|
||||
|
||||
/// The text style for [noRecentsText]
|
||||
final TextStyle noRecentsStyle;
|
||||
|
||||
/// Duration of tab indicator to animate to next category
|
||||
final Duration tabIndicatorAnimDuration;
|
||||
|
||||
/// Determines the icon to display for each [Category]
|
||||
final CategoryIcons categoryIcons;
|
||||
|
||||
/// Change between Material and Cupertino button style
|
||||
final ButtonMode buttonMode;
|
||||
|
||||
/// Get Emoji size based on properties and screen width
|
||||
double getEmojiSize(double width) {
|
||||
final maxSize = width / columns;
|
||||
return min(maxSize, emojiSizeMax);
|
||||
}
|
||||
|
||||
/// Returns the icon for the category
|
||||
IconData getIconForCategory(Category category) {
|
||||
switch (category) {
|
||||
case Category.RECENT:
|
||||
return categoryIcons.recentIcon;
|
||||
case Category.SMILEYS:
|
||||
return categoryIcons.smileyIcon;
|
||||
case Category.ANIMALS:
|
||||
return categoryIcons.animalIcon;
|
||||
case Category.FOODS:
|
||||
return categoryIcons.foodIcon;
|
||||
case Category.TRAVEL:
|
||||
return categoryIcons.travelIcon;
|
||||
case Category.ACTIVITIES:
|
||||
return categoryIcons.activityIcon;
|
||||
case Category.OBJECTS:
|
||||
return categoryIcons.objectIcon;
|
||||
case Category.SYMBOLS:
|
||||
return categoryIcons.symbolIcon;
|
||||
case Category.FLAGS:
|
||||
return categoryIcons.flagIcon;
|
||||
case Category.SEARCH:
|
||||
return categoryIcons.searchIcon;
|
||||
default:
|
||||
throw Exception('Unsupported Category');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return (other is Config) &&
|
||||
other.columns == columns &&
|
||||
other.emojiSizeMax == emojiSizeMax &&
|
||||
other.verticalSpacing == verticalSpacing &&
|
||||
other.horizontalSpacing == horizontalSpacing &&
|
||||
other.initCategory == initCategory &&
|
||||
other.bgColor == bgColor &&
|
||||
other.indicatorColor == indicatorColor &&
|
||||
other.iconColor == iconColor &&
|
||||
other.iconColorSelected == iconColorSelected &&
|
||||
other.progressIndicatorColor == progressIndicatorColor &&
|
||||
other.backspaceColor == backspaceColor &&
|
||||
other.showRecentsTab == showRecentsTab &&
|
||||
other.recentsLimit == recentsLimit &&
|
||||
other.noRecentsText == noRecentsText &&
|
||||
other.noRecentsStyle == noRecentsStyle &&
|
||||
other.tabIndicatorAnimDuration == tabIndicatorAnimDuration &&
|
||||
other.categoryIcons == categoryIcons &&
|
||||
other.buttonMode == buttonMode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
columns.hashCode ^
|
||||
emojiSizeMax.hashCode ^
|
||||
verticalSpacing.hashCode ^
|
||||
horizontalSpacing.hashCode ^
|
||||
initCategory.hashCode ^
|
||||
bgColor.hashCode ^
|
||||
indicatorColor.hashCode ^
|
||||
iconColor.hashCode ^
|
||||
iconColorSelected.hashCode ^
|
||||
progressIndicatorColor.hashCode ^
|
||||
backspaceColor.hashCode ^
|
||||
showRecentsTab.hashCode ^
|
||||
recentsLimit.hashCode ^
|
||||
noRecentsText.hashCode ^
|
||||
noRecentsStyle.hashCode ^
|
||||
tabIndicatorAnimDuration.hashCode ^
|
||||
categoryIcons.hashCode ^
|
||||
buttonMode.hashCode;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorBlockComponentBuilder extends BlockComponentBuilder {
|
||||
ErrorBlockComponentBuilder({
|
||||
super.configuration,
|
||||
});
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return ErrorBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
showActions: showActions(node),
|
||||
actionBuilder: (context, state) => actionBuilder(
|
||||
blockComponentContext,
|
||||
state,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) => true;
|
||||
}
|
||||
|
||||
class ErrorBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
const ErrorBlockComponentWidget({
|
||||
super.key,
|
||||
required super.node,
|
||||
super.showActions,
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
});
|
||||
|
||||
@override
|
||||
State<ErrorBlockComponentWidget> createState() =>
|
||||
_DividerBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
||||
with BlockComponentConfigurable {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyButton(
|
||||
onTap: () async {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
|
||||
);
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: jsonEncode(node.toJson())),
|
||||
);
|
||||
},
|
||||
text: Container(
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
child = Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -24,7 +24,7 @@ class FindAndReplaceMenuWidget extends StatefulWidget {
|
||||
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
||||
bool showReplaceMenu = false;
|
||||
|
||||
late SearchServiceV2 searchService = SearchServiceV2(
|
||||
late SearchServiceV3 searchService = SearchServiceV3(
|
||||
editorState: widget.editorState,
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ class FindMenu extends StatefulWidget {
|
||||
|
||||
final EditorState editorState;
|
||||
final VoidCallback onDismiss;
|
||||
final SearchServiceV2 searchService;
|
||||
final SearchServiceV3 searchService;
|
||||
final void Function(bool value) onShowReplace;
|
||||
|
||||
@override
|
||||
@@ -93,7 +93,7 @@ class _FindMenuState extends State<FindMenu> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.searchService.matchedPositions.addListener(_setState);
|
||||
widget.searchService.matchWrappers.addListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.addListener(_setState);
|
||||
|
||||
findTextEditingController.addListener(_searchPattern);
|
||||
@@ -105,7 +105,7 @@ class _FindMenuState extends State<FindMenu> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.searchService.matchedPositions.removeListener(_setState);
|
||||
widget.searchService.matchWrappers.removeListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
||||
widget.searchService.dispose();
|
||||
findTextEditingController.removeListener(_searchPattern);
|
||||
@@ -117,7 +117,7 @@ class _FindMenuState extends State<FindMenu> {
|
||||
Widget build(BuildContext context) {
|
||||
// the selectedIndex from searchService is 0-based
|
||||
final selectedIndex = widget.searchService.selectedIndex + 1;
|
||||
final matches = widget.searchService.matchedPositions.value;
|
||||
final matches = widget.searchService.matchWrappers.value;
|
||||
return Row(
|
||||
children: [
|
||||
const HSpace(4.0),
|
||||
@@ -232,7 +232,7 @@ class ReplaceMenu extends StatefulWidget {
|
||||
/// The localizations of the find and replace menu
|
||||
final FindReplaceLocalizations? localizations;
|
||||
|
||||
final SearchServiceV2 searchService;
|
||||
final SearchServiceV3 searchService;
|
||||
|
||||
@override
|
||||
State<ReplaceMenu> createState() => _ReplaceMenuState();
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
id: 'editor.font',
|
||||
group: 4,
|
||||
isActive: onlyShowInTextType,
|
||||
builder: (context, editorState, highlightColor) {
|
||||
builder: (context, editorState, highlightColor, _) {
|
||||
final selection = editorState.selection!;
|
||||
final popoverController = PopoverController();
|
||||
return MouseRegion(
|
||||
@@ -18,11 +18,11 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
currentFontFamily: '',
|
||||
offset: const Offset(0, 12),
|
||||
popoverController: popoverController,
|
||||
onOpen: () => keepEditorFocusNotifier.value += 1,
|
||||
onClose: () => keepEditorFocusNotifier.value -= 1,
|
||||
onOpen: () => keepEditorFocusNotifier.increase(),
|
||||
onClose: () => keepEditorFocusNotifier.decrease(),
|
||||
showResetButton: true,
|
||||
onFontFamilyChanged: (fontFamily) async {
|
||||
await popoverController.close();
|
||||
popoverController.close();
|
||||
try {
|
||||
await editorState.formatDelta(selection, {
|
||||
AppFlowyRichTextKeys.fontFamily: fontFamily,
|
||||
|
||||
+3
-8
@@ -484,14 +484,9 @@ class _ImageGridItemState extends State<ImageGridItem> {
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: widget.onImageSelect,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: FileImage(File(widget.imagePath)),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: Corners.s8Border,
|
||||
child: Image.file(File(widget.imagePath), fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
if (showDeleteButton)
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -257,7 +257,7 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
constraints: BoxConstraints.loose(const Size(320, 380)),
|
||||
constraints: BoxConstraints.loose(const Size(300, 250)),
|
||||
child: FlowyButton(
|
||||
leftIconSize: const Size.square(18),
|
||||
useIntrinsicWidth: true,
|
||||
|
||||
+3
-12
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
@@ -8,6 +8,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Add icon menu in Header
|
||||
class EmojiPopover extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
final Node node;
|
||||
@@ -46,17 +47,7 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
config: buildFlowyEmojiPickerConfig(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const kImagePlaceholderKey = 'imagePlaceholderKey';
|
||||
|
||||
typedef CustomImageBlockComponentMenuBuilder = Widget Function(
|
||||
Node node,
|
||||
CustomImageBlockComponentState state,
|
||||
);
|
||||
|
||||
class CustomImageBlockComponentBuilder extends BlockComponentBuilder {
|
||||
CustomImageBlockComponentBuilder({
|
||||
super.configuration,
|
||||
this.showMenu = false,
|
||||
this.menuBuilder,
|
||||
});
|
||||
|
||||
/// Whether to show the menu of this block component.
|
||||
final bool showMenu;
|
||||
|
||||
///
|
||||
final CustomImageBlockComponentMenuBuilder? menuBuilder;
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return CustomImageBlockComponent(
|
||||
key: node.key,
|
||||
node: node,
|
||||
showActions: showActions(node),
|
||||
configuration: configuration,
|
||||
actionBuilder: (context, state) => actionBuilder(
|
||||
blockComponentContext,
|
||||
state,
|
||||
),
|
||||
showMenu: showMenu,
|
||||
menuBuilder: menuBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) => node.delta == null && node.children.isEmpty;
|
||||
}
|
||||
|
||||
class CustomImageBlockComponent extends BlockComponentStatefulWidget {
|
||||
const CustomImageBlockComponent({
|
||||
super.key,
|
||||
required super.node,
|
||||
super.showActions,
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
this.showMenu = false,
|
||||
this.menuBuilder,
|
||||
});
|
||||
|
||||
/// Whether to show the menu of this block component.
|
||||
final bool showMenu;
|
||||
|
||||
final CustomImageBlockComponentMenuBuilder? menuBuilder;
|
||||
|
||||
@override
|
||||
State<CustomImageBlockComponent> createState() =>
|
||||
CustomImageBlockComponentState();
|
||||
}
|
||||
|
||||
class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
|
||||
with SelectableMixin, BlockComponentConfigurable {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
final imageKey = GlobalKey();
|
||||
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
late final editorState = Provider.of<EditorState>(context, listen: false);
|
||||
|
||||
final showActionsNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
bool alwaysShowMenu = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final node = widget.node;
|
||||
final attributes = node.attributes;
|
||||
final src = attributes[ImageBlockKeys.url];
|
||||
|
||||
final alignment = AlignmentExtension.fromString(
|
||||
attributes[ImageBlockKeys.align] ?? 'center',
|
||||
);
|
||||
final width = attributes[ImageBlockKeys.width]?.toDouble() ??
|
||||
MediaQuery.of(context).size.width;
|
||||
final height = attributes[ImageBlockKeys.height]?.toDouble();
|
||||
|
||||
final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey];
|
||||
Widget child = src.isEmpty
|
||||
? ImagePlaceholder(
|
||||
key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,
|
||||
node: node,
|
||||
)
|
||||
: ResizableImage(
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
editable: editorState.editable,
|
||||
alignment: alignment,
|
||||
onResize: (width) {
|
||||
final transaction = editorState.transaction
|
||||
..updateNode(node, {
|
||||
ImageBlockKeys.width: width,
|
||||
});
|
||||
editorState.apply(transaction);
|
||||
},
|
||||
);
|
||||
|
||||
child = BlockSelectionContainer(
|
||||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [
|
||||
BlockSelectionType.block,
|
||||
],
|
||||
child: Padding(
|
||||
key: imageKey,
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.showMenu && widget.menuBuilder != null) {
|
||||
child = MouseRegion(
|
||||
onEnter: (_) => showActionsNotifier.value = true,
|
||||
onExit: (_) {
|
||||
if (!alwaysShowMenu) {
|
||||
showActionsNotifier.value = false;
|
||||
}
|
||||
},
|
||||
hitTestBehavior: HitTestBehavior.opaque,
|
||||
opaque: false,
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: showActionsNotifier,
|
||||
builder: (context, value, child) {
|
||||
final url = node.attributes[ImageBlockKeys.url];
|
||||
return Stack(
|
||||
children: [
|
||||
BlockSelectionContainer(
|
||||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
child: child!,
|
||||
),
|
||||
if (value && url.isNotEmpty == true)
|
||||
widget.menuBuilder!(
|
||||
widget.node,
|
||||
this,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() => Position(path: widget.node.path, offset: 0);
|
||||
|
||||
@override
|
||||
Position end() => Position(path: widget.node.path, offset: 1);
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.cover;
|
||||
|
||||
@override
|
||||
Rect getBlockRect({
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final imageBox = imageKey.currentContext?.findRenderObject();
|
||||
if (imageBox is RenderBox) {
|
||||
return Offset.zero & imageBox.size;
|
||||
}
|
||||
return Rect.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(
|
||||
Position position, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
final rects = getRectsInSelection(Selection.collapsed(position));
|
||||
return rects.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(
|
||||
Selection selection, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) {
|
||||
if (_renderBox == null) {
|
||||
return [];
|
||||
}
|
||||
final parentBox = context.findRenderObject();
|
||||
final imageBox = imageKey.currentContext?.findRenderObject();
|
||||
if (parentBox is RenderBox && imageBox is RenderBox) {
|
||||
return [
|
||||
imageBox.localToGlobal(Offset.zero, ancestor: parentBox) &
|
||||
imageBox.size,
|
||||
];
|
||||
}
|
||||
return [Offset.zero & _renderBox!.size];
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(
|
||||
Offset offset, {
|
||||
bool shiftWithBaseOffset = false,
|
||||
}) =>
|
||||
_renderBox!.localToGlobal(offset);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmbedImageUrlWidget extends StatefulWidget {
|
||||
const EmbedImageUrlWidget({
|
||||
super.key,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
final void Function(String url) onSubmit;
|
||||
|
||||
@override
|
||||
State<EmbedImageUrlWidget> createState() => _EmbedImageUrlWidgetState();
|
||||
}
|
||||
|
||||
class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
|
||||
String inputText = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
FlowyTextField(
|
||||
autoFocus: true,
|
||||
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
|
||||
onChanged: (value) => inputText = value,
|
||||
onEditingComplete: () => widget.onSubmit(inputText),
|
||||
),
|
||||
const VSpace(5),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onTap: () => widget.onSubmit(inputText),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@@ -18,7 +19,7 @@ class ImageMenu extends StatefulWidget {
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final ImageBlockComponentWidgetState state;
|
||||
final CustomImageBlockComponentState state;
|
||||
|
||||
@override
|
||||
State<ImageMenu> createState() => _ImageMenuState();
|
||||
@@ -109,7 +110,7 @@ class _ImageAlignButton extends StatefulWidget {
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final ImageBlockComponentWidgetState state;
|
||||
final CustomImageBlockComponentState state;
|
||||
|
||||
@override
|
||||
State<_ImageAlignButton> createState() => _ImageAlignButtonState();
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log, UploadImageMenu;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.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:http/http.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
class ImagePlaceholder extends StatefulWidget {
|
||||
const ImagePlaceholder({
|
||||
super.key,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
State<ImagePlaceholder> createState() => ImagePlaceholderState();
|
||||
}
|
||||
|
||||
class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
final controller = PopoverController();
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: controller,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 540,
|
||||
maxHeight: 360,
|
||||
minHeight: 80,
|
||||
),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (context) {
|
||||
return UploadImageMenu(
|
||||
onSelectedLocalImage: (path) {
|
||||
controller.close();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await insertLocalImage(path);
|
||||
});
|
||||
},
|
||||
onSelectedAIImage: (url) {
|
||||
controller.close();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await insertAIImage(url);
|
||||
});
|
||||
},
|
||||
onSelectedNetworkImage: (url) {
|
||||
controller.close();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await insertNetworkImage(url);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(10),
|
||||
const FlowySvg(
|
||||
FlowySvgs.image_placeholder_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
const HSpace(10),
|
||||
FlowyText(
|
||||
LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertLocalImage(String? url) async {
|
||||
if (url == null || url.isEmpty) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||
final imagePath = p.join(
|
||||
path,
|
||||
'images',
|
||||
);
|
||||
try {
|
||||
// create the directory if not exists
|
||||
final directory = Directory(imagePath);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
final copyToPath = p.join(
|
||||
imagePath,
|
||||
'${uuid()}${p.extension(url)}',
|
||||
);
|
||||
await File(url).copy(
|
||||
copyToPath,
|
||||
);
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
ImageBlockKeys.url: copyToPath,
|
||||
});
|
||||
await editorState.apply(transaction);
|
||||
} catch (e) {
|
||||
Log.error('cannot copy image file', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertAIImage(String url) async {
|
||||
if (url.isEmpty || !isURL(url)) {
|
||||
// show error
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||
final imagePath = p.join(
|
||||
path,
|
||||
'images',
|
||||
);
|
||||
try {
|
||||
// create the directory if not exists
|
||||
final directory = Directory(imagePath);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
final uri = Uri.parse(url);
|
||||
final copyToPath = p.join(
|
||||
imagePath,
|
||||
'${uuid()}${p.extension(uri.path)}',
|
||||
);
|
||||
|
||||
final response = await get(uri);
|
||||
await File(copyToPath).writeAsBytes(response.bodyBytes);
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
ImageBlockKeys.url: copyToPath,
|
||||
});
|
||||
await editorState.apply(transaction);
|
||||
} catch (e) {
|
||||
Log.error('cannot save image file', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertNetworkImage(String url) async {
|
||||
if (url.isEmpty || !isURL(url)) {
|
||||
// show error
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.updateNode(widget.node, {
|
||||
ImageBlockKeys.url: url,
|
||||
});
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
+51
-45
@@ -1,12 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
final customImageMenuItem = SelectionMenuItem(
|
||||
name: AppFlowyEditorLocalizations.current.image,
|
||||
@@ -16,44 +11,55 @@ final customImageMenuItem = SelectionMenuItem(
|
||||
style: style,
|
||||
),
|
||||
keywords: ['image', 'picture', 'img', 'photo'],
|
||||
handler: (editorState, menuService, context) {
|
||||
final container = Overlay.of(context);
|
||||
showImageMenu(
|
||||
container,
|
||||
editorState,
|
||||
menuService,
|
||||
onInsertImage: (url) async {
|
||||
// if the url is http, we can insert it directly
|
||||
// otherwise, if it's a file url, we need to copy the file to the app's document directory
|
||||
handler: (editorState, menuService, context) async {
|
||||
// use the key to retrieve the state of the image block to show the popover automatically
|
||||
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
|
||||
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
|
||||
|
||||
final regex = RegExp('^(http|https)://');
|
||||
if (regex.hasMatch(url)) {
|
||||
await editorState.insertImageNode(url);
|
||||
} else {
|
||||
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||
final imagePath = p.join(
|
||||
path,
|
||||
'images',
|
||||
);
|
||||
try {
|
||||
// create the directory if not exists
|
||||
final directory = Directory(imagePath);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
final copyToPath = p.join(
|
||||
imagePath,
|
||||
'${uuid()}${p.extension(url)}',
|
||||
);
|
||||
await File(url).copy(
|
||||
copyToPath,
|
||||
);
|
||||
await editorState.insertImageNode(copyToPath);
|
||||
} catch (e) {
|
||||
Log.error('cannot copy image file', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
imagePlaceholderKey.currentState?.controller.show();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
extension InsertImage on EditorState {
|
||||
Future<void> insertEmptyImageBlock(GlobalKey key) async {
|
||||
final selection = this.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final node = getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final emptyImage = imageNode(url: '')
|
||||
..extraInfos = {
|
||||
kImagePlaceholderKey: key,
|
||||
};
|
||||
final transaction = this.transaction;
|
||||
// if the current node is empty paragraph, replace it with image node
|
||||
if (node.type == ParagraphBlockKeys.type &&
|
||||
(node.delta?.isEmpty ?? false)) {
|
||||
transaction
|
||||
..insertNode(
|
||||
node.path,
|
||||
emptyImage,
|
||||
)
|
||||
..deleteNode(node);
|
||||
} else {
|
||||
transaction.insertNode(
|
||||
node.path.next,
|
||||
emptyImage,
|
||||
);
|
||||
}
|
||||
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path.next,
|
||||
offset: 0,
|
||||
),
|
||||
);
|
||||
|
||||
return apply(transaction);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:dartz/dartz.dart' hide State;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OpenAIImageWidget extends StatefulWidget {
|
||||
const OpenAIImageWidget({
|
||||
super.key,
|
||||
required this.onSelectNetworkImage,
|
||||
});
|
||||
|
||||
final void Function(String url) onSelectNetworkImage;
|
||||
|
||||
@override
|
||||
State<OpenAIImageWidget> createState() => _OpenAIImageWidgetState();
|
||||
}
|
||||
|
||||
class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
|
||||
Future<Either<OpenAIError, List<String>>>? future;
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
autoFocus: true,
|
||||
hintText: LocaleKeys.document_imageBlock_ai_placeholder.tr(),
|
||||
onChanged: (value) => query = value,
|
||||
onEditingComplete: _search,
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
if (future != null)
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, value) {
|
||||
final data = value.data;
|
||||
if (!value.hasData ||
|
||||
value.connectionState != ConnectionState.done ||
|
||||
data == null) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return data.fold(
|
||||
(l) => Center(
|
||||
child: FlowyText(
|
||||
l.message,
|
||||
maxLines: 3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
(r) => GridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 16.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childAspectRatio: 4 / 3,
|
||||
children: r
|
||||
.map(
|
||||
(e) => GestureDetector(
|
||||
onTap: () => widget.onSelectNetworkImage(e),
|
||||
child: Image.network(e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _search() async {
|
||||
final openAI = await getIt.getAsync<OpenAIRepository>();
|
||||
setState(() {
|
||||
future = openAI.generateImage(
|
||||
prompt: query,
|
||||
n: 6,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:dartz/dartz.dart' hide State;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class StabilityAIImageWidget extends StatefulWidget {
|
||||
const StabilityAIImageWidget({
|
||||
super.key,
|
||||
required this.onSelectImage,
|
||||
});
|
||||
|
||||
final void Function(String url) onSelectImage;
|
||||
|
||||
@override
|
||||
State<StabilityAIImageWidget> createState() => _StabilityAIImageWidgetState();
|
||||
}
|
||||
|
||||
class _StabilityAIImageWidgetState extends State<StabilityAIImageWidget> {
|
||||
Future<Either<StabilityAIRequestError, List<String>>>? future;
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
autoFocus: true,
|
||||
hintText: LocaleKeys
|
||||
.document_imageBlock_stability_ai_placeholder
|
||||
.tr(),
|
||||
onChanged: (value) => query = value,
|
||||
onEditingComplete: _search,
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
if (future != null)
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, value) {
|
||||
final data = value.data;
|
||||
if (!value.hasData ||
|
||||
value.connectionState != ConnectionState.done ||
|
||||
data == null) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return data.fold(
|
||||
(l) => Center(
|
||||
child: FlowyText(
|
||||
l.message,
|
||||
maxLines: 3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
(r) => GridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 16.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childAspectRatio: 4 / 3,
|
||||
children: r.map(
|
||||
(e) {
|
||||
final base64Image = base64Decode(e);
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final path = p.join(
|
||||
tempDirectory.path,
|
||||
'${uuid()}.png',
|
||||
);
|
||||
File(path).writeAsBytesSync(base64Image);
|
||||
widget.onSelectImage(path);
|
||||
},
|
||||
child: Image.memory(base64Image),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _search() async {
|
||||
final stabilityAI = await getIt.getAsync<StabilityAIRepository>();
|
||||
setState(() {
|
||||
future = stabilityAI.generateImage(
|
||||
prompt: query,
|
||||
n: 6,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:unsplash_client/unsplash_client.dart';
|
||||
|
||||
class UnsplashImageWidget extends StatefulWidget {
|
||||
const UnsplashImageWidget({
|
||||
super.key,
|
||||
required this.onSelectUnsplashImage,
|
||||
});
|
||||
|
||||
final void Function(String url) onSelectUnsplashImage;
|
||||
|
||||
@override
|
||||
State<UnsplashImageWidget> createState() => _UnsplashImageWidgetState();
|
||||
}
|
||||
|
||||
class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
|
||||
final client = UnsplashClient(
|
||||
settings: const ClientSettings(
|
||||
credentials: AppCredentials(
|
||||
// TODO: there're the demo keys, we should replace them with the production keys when releasing and inject them with env file.
|
||||
accessKey: 'YyD-LbW5bVolHWZBq5fWRM_3ezkG2XchRFjhNTnK9TE',
|
||||
secretKey: '5z4EnxaXjWjWMnuBhc0Ku0uYW2bsYCZlO-REZaqmV6A',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
late Future<List<Photo>> randomPhotos;
|
||||
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
randomPhotos = client.photos
|
||||
.random(count: 18, orientation: PhotoOrientation.landscape)
|
||||
.goAndGet();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
client.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
autoFocus: true,
|
||||
hintText: LocaleKeys.document_imageBlock_searchForAnImage.tr(),
|
||||
onChanged: (value) => query = value,
|
||||
onEditingComplete: _search,
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: randomPhotos,
|
||||
builder: (context, value) {
|
||||
final data = value.data;
|
||||
if (!value.hasData ||
|
||||
value.connectionState != ConnectionState.done ||
|
||||
data == null ||
|
||||
data.isEmpty) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return GridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 16.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childAspectRatio: 4 / 3,
|
||||
children: data
|
||||
.map(
|
||||
(photo) => _UnsplashImage(
|
||||
photo: photo,
|
||||
onTap: () => widget.onSelectUnsplashImage(
|
||||
photo.urls.regular.toString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _search() {
|
||||
setState(() {
|
||||
randomPhotos = client.photos
|
||||
.random(
|
||||
count: 18,
|
||||
orientation: PhotoOrientation.landscape,
|
||||
query: query,
|
||||
)
|
||||
.goAndGet();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _UnsplashImage extends StatelessWidget {
|
||||
const _UnsplashImage({
|
||||
required this.photo,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final Photo photo;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Image.network(
|
||||
photo.urls.thumb.toString(),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowyText(
|
||||
'by ${photo.name}',
|
||||
fontSize: 10.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Photo {
|
||||
String get name {
|
||||
if (user.username.isNotEmpty) {
|
||||
return user.username;
|
||||
}
|
||||
|
||||
if (user.name.isNotEmpty) {
|
||||
return user.name;
|
||||
}
|
||||
|
||||
if (user.email?.isNotEmpty == true) {
|
||||
return user.email!;
|
||||
}
|
||||
|
||||
return user.id;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class UploadImageFileWidget extends StatelessWidget {
|
||||
const UploadImageFileWidget({
|
||||
super.key,
|
||||
required this.onPickFile,
|
||||
this.allowedExtensions = const ['jpg', 'png', 'jpeg'],
|
||||
});
|
||||
|
||||
final void Function(String? path) onPickFile;
|
||||
final List<String> allowedExtensions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyHover(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (_) async {
|
||||
final result = await getIt<FilePickerService>().pickFiles(
|
||||
dialogTitle: '',
|
||||
allowMultiple: false,
|
||||
type: FileType.image,
|
||||
allowedExtensions: allowedExtensions,
|
||||
);
|
||||
onPickFile(result?.files.firstOrNull?.path);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum UploadImageType {
|
||||
local,
|
||||
url,
|
||||
unsplash,
|
||||
stabilityAI,
|
||||
openAI;
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case UploadImageType.local:
|
||||
return LocaleKeys.document_imageBlock_upload_label.tr();
|
||||
case UploadImageType.url:
|
||||
return LocaleKeys.document_imageBlock_embedLink_label.tr();
|
||||
case UploadImageType.unsplash:
|
||||
return 'Unsplash';
|
||||
case UploadImageType.openAI:
|
||||
return LocaleKeys.document_imageBlock_ai_label.tr();
|
||||
case UploadImageType.stabilityAI:
|
||||
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UploadImageMenu extends StatefulWidget {
|
||||
const UploadImageMenu({
|
||||
super.key,
|
||||
required this.onSelectedLocalImage,
|
||||
required this.onSelectedAIImage,
|
||||
required this.onSelectedNetworkImage,
|
||||
});
|
||||
|
||||
final void Function(String? path) onSelectedLocalImage;
|
||||
final void Function(String url) onSelectedAIImage;
|
||||
final void Function(String url) onSelectedNetworkImage;
|
||||
|
||||
@override
|
||||
State<UploadImageMenu> createState() => _UploadImageMenuState();
|
||||
}
|
||||
|
||||
class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
int currentTabIndex = 0;
|
||||
List<UploadImageType> values = UploadImageType.values;
|
||||
bool supportOpenAI = false;
|
||||
bool supportStabilityAI = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
UserBackendService.getCurrentUserProfile().then(
|
||||
(value) {
|
||||
final supportOpenAI = value.fold(
|
||||
(l) => false,
|
||||
(r) => r.openaiKey.isNotEmpty,
|
||||
);
|
||||
final supportStabilityAI = value.fold(
|
||||
(l) => false,
|
||||
(r) => r.stabilityAiKey.isNotEmpty,
|
||||
);
|
||||
if (supportOpenAI != this.supportOpenAI ||
|
||||
supportStabilityAI != this.supportStabilityAI) {
|
||||
setState(() {
|
||||
this.supportOpenAI = supportOpenAI;
|
||||
this.supportStabilityAI = supportStabilityAI;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: values.length,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TabBar(
|
||||
onTap: (value) => setState(() {
|
||||
currentTabIndex = value;
|
||||
}),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
isScrollable: true,
|
||||
overlayColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
// splashBorderRadius: BorderRadius.circular(4),
|
||||
tabs: values
|
||||
.map(
|
||||
(e) => FlowyHover(
|
||||
style: const HoverStyle(borderRadius: BorderRadius.zero),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: FlowyText(e.description),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const Divider(
|
||||
height: 2,
|
||||
),
|
||||
_buildTab(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab() {
|
||||
final type = UploadImageType.values[currentTabIndex];
|
||||
switch (type) {
|
||||
case UploadImageType.local:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: UploadImageFileWidget(
|
||||
onPickFile: widget.onSelectedLocalImage,
|
||||
),
|
||||
);
|
||||
case UploadImageType.url:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: EmbedImageUrlWidget(
|
||||
onSubmit: widget.onSelectedNetworkImage,
|
||||
),
|
||||
);
|
||||
case UploadImageType.unsplash:
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: UnsplashImageWidget(
|
||||
onSelectUnsplashImage: widget.onSelectedNetworkImage,
|
||||
),
|
||||
),
|
||||
);
|
||||
case UploadImageType.openAI:
|
||||
return supportOpenAI
|
||||
? Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OpenAIImageWidget(
|
||||
onSelectNetworkImage: widget.onSelectedAIImage,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
|
||||
),
|
||||
);
|
||||
case UploadImageType.stabilityAI:
|
||||
return supportStabilityAI
|
||||
? Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StabilityAIImageWidget(
|
||||
onSelectImage: widget.onSelectedLocalImage,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_imageBlock_pleaseInputYourStabilityAIKey
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -3,14 +3,13 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final ToolbarItem inlineMathEquationItem = ToolbarItem(
|
||||
id: 'editor.inline_math_equation',
|
||||
group: 2,
|
||||
isActive: onlyShowInSingleSelectionAndTextType,
|
||||
builder: (context, editorState, highlightColor) {
|
||||
builder: (context, editorState, highlightColor, _) {
|
||||
final selection = editorState.selection!;
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {
|
||||
|
||||
+13
-5
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -82,11 +82,13 @@ class MentionDateBlock extends StatelessWidget {
|
||||
_updateBlock(parsedDate!.withoutTime, includeTime);
|
||||
|
||||
// We can remove time from the date/reminder
|
||||
// block when toggled off.
|
||||
if (!includeTime && isReminder) {
|
||||
// block when toggled off.
|
||||
if (isReminder) {
|
||||
_updateScheduledAt(
|
||||
reminderId: reminderId!,
|
||||
selectedDay: parsedDate!.withoutTime,
|
||||
selectedDay:
|
||||
includeTime ? parsedDate! : parsedDate!.withoutTime,
|
||||
includeTime: includeTime,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -99,6 +101,7 @@ class MentionDateBlock extends StatelessWidget {
|
||||
_updateScheduledAt(
|
||||
reminderId: reminderId!,
|
||||
selectedDay: selectedDay,
|
||||
includeTime: includeTime,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -171,10 +174,15 @@ class MentionDateBlock extends StatelessWidget {
|
||||
void _updateScheduledAt({
|
||||
required String reminderId,
|
||||
required DateTime selectedDay,
|
||||
bool? includeTime,
|
||||
}) {
|
||||
editorContext.read<ReminderBloc>().add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(id: reminderId, scheduledAt: selectedDay),
|
||||
ReminderUpdate(
|
||||
id: reminderId,
|
||||
scheduledAt: selectedDay,
|
||||
includeTime: includeTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+54
-5
@@ -1,20 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
|
||||
|
||||
import 'text_completion.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'error.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'text_completion.dart';
|
||||
|
||||
// Please fill in your own API key
|
||||
const apiKey = '';
|
||||
|
||||
enum OpenAIRequestType {
|
||||
textCompletion,
|
||||
textEdit;
|
||||
textEdit,
|
||||
imageGenerations;
|
||||
|
||||
Uri get uri {
|
||||
switch (this) {
|
||||
@@ -22,6 +22,8 @@ enum OpenAIRequestType {
|
||||
return Uri.parse('https://api.openai.com/v1/completions');
|
||||
case OpenAIRequestType.textEdit:
|
||||
return Uri.parse('https://api.openai.com/v1/edits');
|
||||
case OpenAIRequestType.imageGenerations:
|
||||
return Uri.parse('https://api.openai.com/v1/images/generations');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +66,17 @@ abstract class OpenAIRepository {
|
||||
required String instruction,
|
||||
double temperature = 0.3,
|
||||
});
|
||||
|
||||
/// Generate image from GPT-3
|
||||
///
|
||||
/// [prompt] is the prompt text
|
||||
/// [n] is the number of images to generate
|
||||
///
|
||||
/// the result is a list of urls
|
||||
Future<Either<OpenAIError, List<String>>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpOpenAIRepository implements OpenAIRepository {
|
||||
@@ -228,4 +241,40 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<OpenAIError, List<String>>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'prompt': prompt,
|
||||
'n': n,
|
||||
'size': '512x512',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await client.post(
|
||||
OpenAIRequestType.imageGenerations.uri,
|
||||
headers: headers,
|
||||
body: json.encode(parameters),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
)['data'] as List;
|
||||
final urls = data
|
||||
.map((e) => e.values)
|
||||
.expand((e) => e)
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
return Right(urls);
|
||||
} else {
|
||||
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||
}
|
||||
} catch (error) {
|
||||
return Left(OpenAIError(message: error.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -14,7 +14,7 @@ final ToolbarItem smartEditItem = ToolbarItem(
|
||||
id: 'appflowy.editor.smart_edit',
|
||||
group: 0,
|
||||
isActive: onlyShowInSingleSelectionAndTextType,
|
||||
builder: (context, editorState, _) => SmartEditActionList(
|
||||
builder: (context, editorState, _, __) => SmartEditActionList(
|
||||
editorState: editorState,
|
||||
),
|
||||
);
|
||||
@@ -55,7 +55,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
actions: SmartEditAction.values
|
||||
.map((action) => SmartEditActionWrapper(action))
|
||||
.toList(),
|
||||
onClosed: () => keepEditorFocusNotifier.decrease(),
|
||||
buildChild: (controller) {
|
||||
keepEditorFocusNotifier.increase();
|
||||
return FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
tooltipText: isOpenAIEnabled
|
||||
|
||||
+4
-11
@@ -68,23 +68,16 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget {
|
||||
}
|
||||
|
||||
class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
||||
with BlockComponentConfigurable, BlockComponentTextDirectionMixin {
|
||||
with
|
||||
BlockComponentConfigurable,
|
||||
BlockComponentTextDirectionMixin,
|
||||
BlockComponentBackgroundColorMixin {
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
// get the background color of the note block from the node's attributes
|
||||
Color get backgroundColor {
|
||||
final colorString =
|
||||
node.attributes[OutlineBlockKeys.backgroundColor] as String?;
|
||||
if (colorString == null) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return colorString.tryToColor() ?? Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
late EditorState editorState = context.read<EditorState>();
|
||||
late Stream<(TransactionTime, Transaction)> stream =
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class CalloutNodeParser extends NodeParser {
|
||||
const CalloutNodeParser();
|
||||
|
||||
@override
|
||||
String get id => CalloutBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
assert(node.children.isEmpty);
|
||||
final icon = node.attributes[CalloutBlockKeys.icon];
|
||||
final delta = node.delta ?? Delta()
|
||||
..insert('');
|
||||
final String markdown = DeltaMarkdownEncoder()
|
||||
.convert(delta)
|
||||
.split('\n')
|
||||
.map((e) => '> $e')
|
||||
.join('\n');
|
||||
return '''
|
||||
> $icon
|
||||
$markdown
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -1,13 +1,23 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class CodeBlockNodeParser extends NodeParser {
|
||||
const CodeBlockNodeParser();
|
||||
|
||||
@override
|
||||
String get id => 'code_block';
|
||||
String get id => CodeBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node) {
|
||||
return '```\n${node.attributes['code_block']}\n```';
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
final delta = node.delta;
|
||||
final language = node.attributes[CodeBlockKeys.language] ?? '';
|
||||
if (delta == null) {
|
||||
throw Exception('Delta is null');
|
||||
}
|
||||
final markdown = DeltaMarkdownEncoder().convert(delta);
|
||||
final result = '```$language\n$markdown\n```';
|
||||
final suffix = node.next == null ? '' : '\n';
|
||||
|
||||
return '$result$suffix';
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class CustomImageNodeParser extends NodeParser {
|
||||
const CustomImageNodeParser();
|
||||
|
||||
@override
|
||||
String get id => ImageBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
assert(node.children.isEmpty);
|
||||
final url = node.attributes[ImageBlockKeys.url];
|
||||
assert(url != null);
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -4,10 +4,10 @@ class DividerNodeParser extends NodeParser {
|
||||
const DividerNodeParser();
|
||||
|
||||
@override
|
||||
String get id => 'divider';
|
||||
String get id => DividerBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node) {
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
return '---\n';
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export 'callout_node_parser.dart';
|
||||
export 'code_block_node_parser.dart';
|
||||
export 'custom_image_node_parser.dart';
|
||||
export 'divider_node_parser.dart';
|
||||
export 'math_equation_node_parser.dart';
|
||||
export 'toggle_list_node_parser.dart';
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export 'callout_node_parser.dart';
|
||||
export 'code_block_node_parser.dart';
|
||||
export 'divider_node_parser.dart';
|
||||
export 'math_equation_node_parser.dart';
|
||||
export 'toggle_list_node_parser.dart';
|
||||
+1
-1
@@ -8,7 +8,7 @@ class MathEquationNodeParser extends NodeParser {
|
||||
String get id => MathEquationBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node) {
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
return '\$\$${node.attributes[id]}\$\$';
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
enum ToggleListExportStyle {
|
||||
github,
|
||||
markdown,
|
||||
}
|
||||
|
||||
class ToggleListNodeParser extends NodeParser {
|
||||
const ToggleListNodeParser({
|
||||
this.exportStyle = ToggleListExportStyle.markdown,
|
||||
});
|
||||
|
||||
final ToggleListExportStyle exportStyle;
|
||||
|
||||
@override
|
||||
String get id => ToggleListBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
final delta = node.delta ?? Delta()
|
||||
..insert('');
|
||||
String markdown = DeltaMarkdownEncoder().convert(delta);
|
||||
final details = encoder?.convertNodes(
|
||||
node.children,
|
||||
withIndent: true,
|
||||
);
|
||||
switch (exportStyle) {
|
||||
case ToggleListExportStyle.github:
|
||||
return '''<details>
|
||||
<summary>$markdown</summary>
|
||||
|
||||
$details
|
||||
</details>
|
||||
''';
|
||||
case ToggleListExportStyle.markdown:
|
||||
markdown = '- $markdown\n';
|
||||
if (details != null && details.isNotEmpty) {
|
||||
markdown += details;
|
||||
}
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -12,7 +12,7 @@ export 'copy_and_paste/custom_paste_command.dart';
|
||||
export 'database/database_view_block_component.dart';
|
||||
export 'database/inline_database_menu_item.dart';
|
||||
export 'database/referenced_database_menu_item.dart';
|
||||
export 'emoji_picker/emoji_menu_item.dart';
|
||||
export 'error/error_block_component_builder.dart';
|
||||
export 'extensions/flowy_tint_extension.dart';
|
||||
export 'find_and_replace/find_and_replace_menu.dart';
|
||||
export 'font/customize_font_toolbar_item.dart';
|
||||
@@ -28,6 +28,7 @@ export 'openai/widgets/auto_completion_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
||||
export 'outline/outline_block_component.dart';
|
||||
export 'parsers/markdown_parsers.dart';
|
||||
export 'table/table_menu.dart';
|
||||
export 'table/table_option_action.dart';
|
||||
export 'toggle/toggle_block_component.dart';
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
enum StabilityAIRequestType {
|
||||
imageGenerations;
|
||||
|
||||
Uri get uri {
|
||||
switch (this) {
|
||||
case StabilityAIRequestType.imageGenerations:
|
||||
return Uri.parse(
|
||||
'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class StabilityAIRepository {
|
||||
/// Generate image from Stability AI
|
||||
///
|
||||
/// [prompt] is the prompt text
|
||||
/// [n] is the number of images to generate
|
||||
///
|
||||
/// the return value is a list of base64 encoded images
|
||||
Future<Either<StabilityAIRequestError, List<String>>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpStabilityAIRepository implements StabilityAIRepository {
|
||||
const HttpStabilityAIRepository({
|
||||
required this.client,
|
||||
required this.apiKey,
|
||||
});
|
||||
|
||||
final http.Client client;
|
||||
final String apiKey;
|
||||
|
||||
Map<String, String> get headers => {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
@override
|
||||
Future<Either<StabilityAIRequestError, List<String>>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'text_prompts': [
|
||||
{
|
||||
'text': prompt,
|
||||
}
|
||||
],
|
||||
'samples': n,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await client.post(
|
||||
StabilityAIRequestType.imageGenerations.uri,
|
||||
headers: headers,
|
||||
body: json.encode(parameters),
|
||||
);
|
||||
|
||||
final data = json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final artifacts = data['artifacts'] as List;
|
||||
final base64Images = artifacts
|
||||
.map(
|
||||
(e) => e['base64'].toString(),
|
||||
)
|
||||
.toList();
|
||||
return Right(base64Images);
|
||||
} else {
|
||||
return Left(
|
||||
StabilityAIRequestError(
|
||||
data['message'].toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return Left(
|
||||
StabilityAIRequestError(
|
||||
error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
class StabilityAIRequestError {
|
||||
final String message;
|
||||
|
||||
StabilityAIRequestError(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StabilityAIRequestError{message: $message}';
|
||||
}
|
||||
}
|
||||
+9
-4
@@ -11,6 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const tableCellDefaultColor = 'appflowy_table_cell_default_color';
|
||||
|
||||
enum TableOptionAction {
|
||||
addAfter,
|
||||
addBefore,
|
||||
@@ -119,12 +121,14 @@ class TableColorOptionAction extends PopoverActionCell {
|
||||
// reset to default background color
|
||||
FlowyColorOption(
|
||||
color: defaultColor,
|
||||
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
|
||||
i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
|
||||
id: tableCellDefaultColor,
|
||||
),
|
||||
...FlowyTint.values.map(
|
||||
(e) => FlowyColorOption(
|
||||
color: e.color(context),
|
||||
name: e.tintName(AppFlowyEditorLocalizations.current),
|
||||
i18n: e.tintName(AppFlowyEditorLocalizations.current),
|
||||
id: e.id,
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -136,8 +140,9 @@ class TableColorOptionAction extends PopoverActionCell {
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
width: 1,
|
||||
),
|
||||
onTap: (color, index) async {
|
||||
final backgroundColor = selectedColor != color ? color.toHex() : "";
|
||||
onTap: (option, index) async {
|
||||
final backgroundColor =
|
||||
selectedColor != option.color ? option.id : '';
|
||||
TableActions.setBgColor(
|
||||
node,
|
||||
position,
|
||||
|
||||
+11
-8
@@ -163,16 +163,19 @@ class _ToggleListBlockComponentWidgetState
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
// the emoji picker button for the note
|
||||
FlowyIconButton(
|
||||
width: 24.0,
|
||||
icon: Icon(
|
||||
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
|
||||
Container(
|
||||
constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: FlowyIconButton(
|
||||
width: 18.0,
|
||||
icon: Icon(
|
||||
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
|
||||
size: 18.0,
|
||||
),
|
||||
onPressed: onCollapsed,
|
||||
),
|
||||
onPressed: onCollapsed,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4.0,
|
||||
),
|
||||
|
||||
Flexible(
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
|
||||
+6
@@ -60,6 +60,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent(
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: selection.start.path, offset: 0),
|
||||
);
|
||||
} else if (selection.startIndex == 0) {
|
||||
// insert a paragraph block above the current toggle list block
|
||||
transaction.insertNode(selection.start.path, paragraphNode());
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: selection.start.path.next, offset: 0),
|
||||
);
|
||||
} else {
|
||||
// insert a toggle list block below the current toggle list block
|
||||
transaction
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
|
||||
import 'package:appflowy/util/google_font_family_extension.dart';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -63,12 +60,13 @@ class EditorStyleCustomizer {
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
code: GoogleFonts.robotoMono(
|
||||
code: GoogleFonts.arefRuqaaInk(
|
||||
textStyle: baseTextStyle(fontFamily).copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.red,
|
||||
backgroundColor: theme.colorScheme.inverseSurface,
|
||||
backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -102,7 +100,7 @@ class EditorStyleCustomizer {
|
||||
color: theme.colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
code: GoogleFonts.robotoMono(
|
||||
code: GoogleFonts.arefRuqaaInk(
|
||||
textStyle: baseTextStyle(fontFamily).copyWith(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.normal,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -16,7 +17,6 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
return PopupMenuButton<int>(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
offset: const Offset(0, 30),
|
||||
tooltip: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
@@ -29,10 +29,13 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
),
|
||||
];
|
||||
},
|
||||
child: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
size: const Size(18, 18),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
size: const Size(18, 18),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ class ShareActionListState extends State<ShareActionList> {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
onPressed: () => controller.show(),
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) async {
|
||||
|
||||
@@ -66,6 +66,7 @@ class DateReferenceService {
|
||||
.toList();
|
||||
|
||||
if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {
|
||||
_setOptions();
|
||||
options = _allOptions;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -9,11 +9,12 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class InlinePageReferenceService {
|
||||
InlinePageReferenceService() {
|
||||
InlinePageReferenceService({required this.currentViewId}) {
|
||||
init();
|
||||
}
|
||||
|
||||
final Completer _initCompleter = Completer<void>();
|
||||
final String currentViewId;
|
||||
|
||||
late final ViewBackendService service;
|
||||
List<InlineActionsMenuItem> _items = [];
|
||||
@@ -22,7 +23,7 @@ class InlinePageReferenceService {
|
||||
Future<void> init() async {
|
||||
service = ViewBackendService();
|
||||
|
||||
_generatePageItems().then((value) {
|
||||
_generatePageItems(currentViewId).then((value) {
|
||||
_items = value;
|
||||
_filtered = value;
|
||||
_initCompleter.complete();
|
||||
@@ -59,7 +60,9 @@ class InlinePageReferenceService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<InlineActionsMenuItem>> _generatePageItems() async {
|
||||
Future<List<InlineActionsMenuItem>> _generatePageItems(
|
||||
String currentViewId,
|
||||
) async {
|
||||
final views = await service.fetchViews();
|
||||
if (views.isEmpty) {
|
||||
return [];
|
||||
@@ -69,6 +72,10 @@ class InlinePageReferenceService {
|
||||
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
|
||||
|
||||
for (final view in views) {
|
||||
if (view.id == currentViewId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final pageSelectionMenuItem = InlineActionsMenuItem(
|
||||
keywords: [view.name.toLowerCase()],
|
||||
label: view.name,
|
||||
|
||||
+5
-1
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -82,6 +83,7 @@ class ReminderReferenceService {
|
||||
.toList();
|
||||
|
||||
if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {
|
||||
_setOptions();
|
||||
options = _allOptions;
|
||||
}
|
||||
}
|
||||
@@ -208,7 +210,9 @@ class ReminderReferenceService {
|
||||
objectId: viewId,
|
||||
title: LocaleKeys.reminderNotification_title.tr(),
|
||||
message: LocaleKeys.reminderNotification_message.tr(),
|
||||
meta: {"document_id": viewId},
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime.name: false.toString(),
|
||||
},
|
||||
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
||||
isAck: date.isBefore(DateTime.now()),
|
||||
);
|
||||
|
||||
@@ -9,12 +9,13 @@ import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/prelude.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
@@ -22,7 +23,8 @@ import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/user/presentation/router.dart';
|
||||
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
@@ -85,6 +87,23 @@ void _resolveCommonService(
|
||||
},
|
||||
);
|
||||
|
||||
getIt.registerFactoryAsync<StabilityAIRepository>(
|
||||
() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold(
|
||||
(l) {
|
||||
throw Exception('Failed to get user profile: ${l.msg}');
|
||||
},
|
||||
(r) {
|
||||
return HttpStabilityAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: r.stabilityAiKey,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
getIt.registerFactory<ClipboardService>(
|
||||
() => ClipboardService(),
|
||||
);
|
||||
@@ -150,14 +169,22 @@ void _resolveHomeDeps(GetIt getIt) {
|
||||
|
||||
getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
|
||||
|
||||
getIt.registerSingleton<ReminderBloc>(ReminderBloc());
|
||||
getIt.registerSingleton<NotificationSettingsCubit>(
|
||||
NotificationSettingsCubit(),
|
||||
);
|
||||
|
||||
getIt.registerSingleton<ReminderBloc>(
|
||||
ReminderBloc(notificationSettings: getIt<NotificationSettingsCubit>()),
|
||||
);
|
||||
}
|
||||
|
||||
void _resolveFolderDeps(GetIt getIt) {
|
||||
//workspace
|
||||
getIt.registerFactoryParam<WorkspaceListener, UserProfilePB, String>(
|
||||
(user, workspaceId) =>
|
||||
WorkspaceListener(user: user, workspaceId: workspaceId),
|
||||
(user, workspaceId) => WorkspaceListener(
|
||||
user: user,
|
||||
workspaceId: workspaceId,
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'deps_resolver.dart';
|
||||
import 'entry_point.dart';
|
||||
import 'launch_configuration.dart';
|
||||
import 'plugin/plugin.dart';
|
||||
import 'tasks/appflowy_cloud_task.dart';
|
||||
import 'tasks/prelude.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
@@ -71,6 +72,7 @@ class FlowyRunner {
|
||||
if (!mode.isUnitTest) ...[
|
||||
const HotKeyTask(),
|
||||
InitSupabaseTask(),
|
||||
InitAppFlowyCloudTask(),
|
||||
const InitAppWidgetTask(),
|
||||
const InitPlatformServiceTask()
|
||||
],
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||
|
||||
import 'prelude.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../user/application/user_settings_service.dart';
|
||||
import '../../workspace/application/appearance.dart';
|
||||
import '../startup.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
|
||||
class InitAppWidgetTask extends LaunchTask {
|
||||
const InitAppWidgetTask();
|
||||
@@ -66,10 +70,13 @@ class InitAppWidgetTask extends LaunchTask {
|
||||
Locale('ru', 'RU'),
|
||||
Locale('sv'),
|
||||
Locale('tr', 'TR'),
|
||||
Locale('uk', 'UA'),
|
||||
Locale('ur'),
|
||||
Locale('vi', 'VN'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('fa'),
|
||||
Locale('hin'),
|
||||
],
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
@@ -93,8 +100,8 @@ class ApplicationWidget extends StatefulWidget {
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final AppearanceSettingsPB appearanceSetting;
|
||||
final AppTheme appTheme;
|
||||
final AppearanceSettingsPB appearanceSetting;
|
||||
final DateTimeSettingsPB dateTimeSettings;
|
||||
|
||||
@override
|
||||
@@ -123,6 +130,9 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
||||
widget.appTheme,
|
||||
)..readLocaleWhenAppLaunch(context),
|
||||
),
|
||||
BlocProvider<NotificationSettingsCubit>(
|
||||
create: (_) => getIt<NotificationSettingsCubit>(),
|
||||
),
|
||||
BlocProvider<DocumentAppearanceCubit>(
|
||||
create: (_) => DocumentAppearanceCubit()..fetch(),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/user_auth_listener.dart';
|
||||
|
||||
class InitAppFlowyCloudTask extends LaunchTask {
|
||||
final _authStateListener = UserAuthStateListener();
|
||||
bool isLoggingOut = false;
|
||||
|
||||
@override
|
||||
Future<void> initialize(LaunchContext context) async {
|
||||
if (!isAppFlowyCloudEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_authStateListener.start(
|
||||
didSignIn: () {
|
||||
isLoggingOut = false;
|
||||
},
|
||||
onInvalidAuth: (message) async {
|
||||
await getIt<AuthService>().signOut();
|
||||
// TODO(nathan): Show a dialog to notify the user that the session is expired.
|
||||
if (!isLoggingOut) {
|
||||
await runAppFlowy();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ AppFlowyEnv getAppFlowyEnv() {
|
||||
|
||||
final appflowyCloudConfig = AppFlowyCloudConfiguration(
|
||||
base_url: Env.afCloudBaseUrl,
|
||||
base_ws_url: Env.afCloudBaseWSUrl,
|
||||
ws_base_url: Env.afCloudWSBaseUrl,
|
||||
gotrue_url: Env.afCloudGoTrueUrl,
|
||||
);
|
||||
|
||||
return AppFlowyEnv(
|
||||
|
||||
@@ -28,7 +28,7 @@ SupbaseRealtimeService? realtimeService;
|
||||
class InitSupabaseTask extends LaunchTask {
|
||||
@override
|
||||
Future<void> initialize(LaunchContext context) async {
|
||||
if (!isCloudEnabled) {
|
||||
if (!isSupabaseEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:appflowy/user/application/auth/backend_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'auth_error.dart';
|
||||
import 'device_id.dart';
|
||||
|
||||
class AFCloudAuthService implements AuthService {
|
||||
final _appLinks = AppLinks();
|
||||
StreamSubscription<Uri?>? _deeplinkSubscription;
|
||||
|
||||
AFCloudAuthService();
|
||||
|
||||
final BackendAuthService _backendAuthService = BackendAuthService(
|
||||
@@ -38,8 +48,72 @@ class AFCloudAuthService implements AuthService {
|
||||
required String platform,
|
||||
Map<String, String> params = const {},
|
||||
}) async {
|
||||
//
|
||||
throw UnimplementedError();
|
||||
final provider = ProviderTypePBExtension.fromPlatform(platform);
|
||||
|
||||
// Get the oauth url from the backend
|
||||
final result = await UserEventGetOauthURLWithProvider(
|
||||
OauthProviderPB.create()..provider = provider,
|
||||
).send();
|
||||
|
||||
return result.fold(
|
||||
(data) async {
|
||||
// Open the webview with oauth url
|
||||
final uri = Uri.parse(data.oauthUrl);
|
||||
final isSuccess = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
webOnlyWindowName: '_self',
|
||||
);
|
||||
|
||||
final completer = Completer<Either<FlowyError, UserProfilePB>>();
|
||||
_deeplinkSubscription = _appLinks.uriLinkStream.listen(
|
||||
(Uri? uri) async {
|
||||
await _handleUri(uri, completer);
|
||||
},
|
||||
onError: (Object err, StackTrace stackTrace) {
|
||||
Log.error('onDeepLinkError: ${err.toString()}', stackTrace);
|
||||
_deeplinkSubscription?.cancel();
|
||||
completer.complete(left(AuthError.deeplinkError));
|
||||
},
|
||||
);
|
||||
|
||||
if (!isSuccess) {
|
||||
_deeplinkSubscription?.cancel();
|
||||
completer.complete(left(AuthError.signInWithOauthError));
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
},
|
||||
(r) => left(r),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleUri(
|
||||
Uri? uri,
|
||||
Completer<Either<FlowyError, UserProfilePB>> completer,
|
||||
) async {
|
||||
if (uri != null) {
|
||||
if (_isAuthCallbackDeeplink(uri)) {
|
||||
// Sign in with url
|
||||
final deviceId = await getDeviceId();
|
||||
final payload = OauthSignInPB(
|
||||
authType: AuthTypePB.AFCloud,
|
||||
map: {
|
||||
AuthServiceMapKeys.signInURL: uri.toString(),
|
||||
AuthServiceMapKeys.deviceId: deviceId
|
||||
},
|
||||
);
|
||||
final result = await UserEventOauthSignIn(payload)
|
||||
.send()
|
||||
.then((value) => value.swap());
|
||||
_deeplinkSubscription?.cancel();
|
||||
completer.complete(result);
|
||||
}
|
||||
} else {
|
||||
Log.error('onDeepLinkError: Unexpect empty deep link callback');
|
||||
_deeplinkSubscription?.cancel();
|
||||
completer.complete(left(AuthError.emptyDeeplink));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -67,3 +141,22 @@ class AFCloudAuthService implements AuthService {
|
||||
return UserBackendService.getCurrentUserProfile();
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderTypePBExtension on ProviderTypePB {
|
||||
static ProviderTypePB fromPlatform(String platform) {
|
||||
switch (platform) {
|
||||
case 'github':
|
||||
return ProviderTypePB.Github;
|
||||
case 'google':
|
||||
return ProviderTypePB.Google;
|
||||
case 'discord':
|
||||
return ProviderTypePB.Discord;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isAuthCallbackDeeplink(Uri uri) {
|
||||
return (uri.fragment.contains('access_token'));
|
||||
}
|
||||
@@ -17,4 +17,16 @@ class AuthError {
|
||||
static final supabaseGetUserError = FlowyError()
|
||||
..msg = 'unable to get user from supabase -10004'
|
||||
..code = ErrorCode.UserUnauthorized;
|
||||
|
||||
static final signInWithOauthError = FlowyError()
|
||||
..msg = 'sign in with oauth error -10003'
|
||||
..code = ErrorCode.UserUnauthorized;
|
||||
|
||||
static final emptyDeeplink = FlowyError()
|
||||
..msg = 'Unexpected empty deeplink'
|
||||
..code = ErrorCode.UnexpectedEmpty;
|
||||
|
||||
static final deeplinkError = FlowyError()
|
||||
..msg = 'Deeplink error'
|
||||
..code = ErrorCode.Internal;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class AuthServiceMapKeys {
|
||||
static const String uuid = 'uuid';
|
||||
static const String email = 'email';
|
||||
static const String deviceId = 'device_id';
|
||||
static const String signInURL = 'sign_in_url';
|
||||
}
|
||||
|
||||
/// `AuthService` is an abstract class that defines methods related to user authentication.
|
||||
|
||||
@@ -169,12 +169,12 @@ class SupabaseAuthService implements AuthService {
|
||||
Future<Either<FlowyError, UserProfilePB>> _setupAuth({
|
||||
required Map<String, String> map,
|
||||
}) async {
|
||||
final payload = OAuthPB(
|
||||
final payload = OauthSignInPB(
|
||||
authType: AuthTypePB.Supabase,
|
||||
map: map,
|
||||
);
|
||||
|
||||
return UserEventOAuth(payload).send().then((value) => value.swap());
|
||||
return UserEventOauthSignIn(payload).send().then((value) => value.swap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 361 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user