Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
||||
@@ -1,5 +1,17 @@
|
||||
# Release Notes
|
||||
|
||||
## 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.5"
|
||||
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
|
||||
@@ -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();
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
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: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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
'Unsplash',
|
||||
);
|
||||
expect(find.byType(UnsplashImageWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
+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 {
|
||||
|
||||
+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(),
|
||||
),
|
||||
);
|
||||
|
||||
+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.delayed(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
|
||||
+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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+1
-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) {
|
||||
|
||||
+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';
|
||||
|
||||
@@ -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,5 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.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';
|
||||
@@ -8,6 +9,7 @@ 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/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 +59,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,6 +151,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
|
||||
// keep the previous font style when typing new text.
|
||||
supportSlashMenuNodeWhiteList.addAll([
|
||||
ToggleListBlockKeys.type,
|
||||
]);
|
||||
AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
|
||||
}
|
||||
|
||||
@@ -264,10 +271,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 +356,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
),
|
||||
),
|
||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
final builders = {
|
||||
|
||||
+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.delayed(
|
||||
preferBelow: false,
|
||||
richMessage: richMessage,
|
||||
child: MouseRegion(
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ 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(
|
||||
@@ -155,7 +156,7 @@ class _AlignButton extends StatelessWidget {
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Tooltip(
|
||||
child: FlowyTooltip.delayed(
|
||||
message: tooltips,
|
||||
child: FlowySvg(
|
||||
icon,
|
||||
|
||||
+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';
|
||||
|
||||
-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;
|
||||
}
|
||||
-338
@@ -1,338 +0,0 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'models/category_models.dart';
|
||||
import 'config.dart';
|
||||
import 'default_emoji_picker_view.dart';
|
||||
import 'models/emoji_model.dart';
|
||||
import 'emoji_lists.dart' as emoji_list;
|
||||
import 'emoji_view_state.dart';
|
||||
import 'models/recent_emoji_model.dart';
|
||||
|
||||
/// All the possible categories that [Emoji] can be put into
|
||||
///
|
||||
/// All [Category] are shown in the category bar
|
||||
enum Category {
|
||||
/// Searched emojis
|
||||
SEARCH,
|
||||
|
||||
/// Recent emojis
|
||||
RECENT,
|
||||
|
||||
/// Smiley emojis
|
||||
SMILEYS,
|
||||
|
||||
/// Animal emojis
|
||||
ANIMALS,
|
||||
|
||||
/// Food emojis
|
||||
FOODS,
|
||||
|
||||
/// Activity emojis
|
||||
ACTIVITIES,
|
||||
|
||||
/// Travel emojis
|
||||
TRAVEL,
|
||||
|
||||
/// Objects emojis
|
||||
OBJECTS,
|
||||
|
||||
/// Sumbol emojis
|
||||
SYMBOLS,
|
||||
|
||||
/// Flag emojis
|
||||
FLAGS,
|
||||
}
|
||||
|
||||
/// Enum to alter the keyboard button style
|
||||
enum ButtonMode {
|
||||
/// Android button style - gives the button a splash color with ripple effect
|
||||
MATERIAL,
|
||||
|
||||
/// iOS button style - gives the button a fade out effect when pressed
|
||||
CUPERTINO
|
||||
}
|
||||
|
||||
/// Callback function for when emoji is selected
|
||||
///
|
||||
/// The function returns the selected [Emoji] as well
|
||||
/// as the [Category] from which it originated
|
||||
typedef OnEmojiSelected = void Function(Category category, Emoji emoji);
|
||||
|
||||
/// Callback function for backspace button
|
||||
typedef OnBackspacePressed = void Function();
|
||||
|
||||
/// Callback function for custom view
|
||||
typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state);
|
||||
|
||||
/// The Emoji Keyboard widget
|
||||
///
|
||||
/// This widget displays a grid of [Emoji] sorted by [Category]
|
||||
/// which the user can horizontally scroll through.
|
||||
///
|
||||
/// There is also a bottombar which displays all the possible [Category]
|
||||
/// and allow the user to quickly switch to that [Category]
|
||||
class EmojiPicker extends StatefulWidget {
|
||||
/// EmojiPicker for flutter
|
||||
const EmojiPicker({
|
||||
Key? key,
|
||||
required this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
this.config = const Config(),
|
||||
this.customWidget,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Custom widget
|
||||
final EmojiViewBuilder? customWidget;
|
||||
|
||||
/// The function called when the emoji is selected
|
||||
final OnEmojiSelected onEmojiSelected;
|
||||
|
||||
/// The function called when backspace button is pressed
|
||||
final OnBackspacePressed? onBackspacePressed;
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
|
||||
@override
|
||||
EmojiPickerState createState() => EmojiPickerState();
|
||||
}
|
||||
|
||||
class EmojiPickerState extends State<EmojiPicker> {
|
||||
static const platform = MethodChannel('emoji_picker_flutter');
|
||||
|
||||
List<CategoryEmoji> categoryEmoji = List.empty(growable: true);
|
||||
List<RecentEmoji> recentEmoji = List.empty(growable: true);
|
||||
late Future<void> updateEmojiFuture;
|
||||
|
||||
// Prevent emojis to be reloaded with every build
|
||||
bool loaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateEmojiFuture = _updateEmojis();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant EmojiPicker oldWidget) {
|
||||
if (oldWidget.config != widget.config) {
|
||||
// Config changed - rebuild EmojiPickerView completely
|
||||
loaded = false;
|
||||
updateEmojiFuture = _updateEmojis();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!loaded) {
|
||||
// Load emojis
|
||||
updateEmojiFuture.then(
|
||||
(value) => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loaded = true;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Show loading indicator
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (widget.config.showRecentsTab) {
|
||||
categoryEmoji[0].emoji =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
}
|
||||
|
||||
final state = EmojiViewState(
|
||||
categoryEmoji,
|
||||
_getOnEmojiListener(),
|
||||
widget.onBackspacePressed,
|
||||
);
|
||||
|
||||
// Build
|
||||
return widget.customWidget == null
|
||||
? DefaultEmojiPickerView(widget.config, state)
|
||||
: widget.customWidget!(widget.config, state);
|
||||
}
|
||||
|
||||
// Add recent emoji handling to tap listener
|
||||
OnEmojiSelected _getOnEmojiListener() {
|
||||
return (category, emoji) {
|
||||
if (widget.config.showRecentsTab) {
|
||||
_addEmojiToRecentlyUsed(emoji).then((value) {
|
||||
if (category != Category.RECENT && mounted) {
|
||||
setState(() {
|
||||
// rebuild to update recent emoji tab
|
||||
// when it is not current tab
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
widget.onEmojiSelected(category, emoji);
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize emoji data
|
||||
Future<void> _updateEmojis() async {
|
||||
categoryEmoji.clear();
|
||||
if (widget.config.showRecentsTab) {
|
||||
recentEmoji = await _getRecentEmojis();
|
||||
final List<Emoji> recentEmojiMap =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
|
||||
}
|
||||
categoryEmoji.addAll([
|
||||
CategoryEmoji(
|
||||
Category.SMILEYS,
|
||||
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ANIMALS,
|
||||
await _getAvailableEmojis(emoji_list.animals, title: 'animals'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FOODS,
|
||||
await _getAvailableEmojis(emoji_list.foods, title: 'foods'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ACTIVITIES,
|
||||
await _getAvailableEmojis(
|
||||
emoji_list.activities,
|
||||
title: 'activities',
|
||||
),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.TRAVEL,
|
||||
await _getAvailableEmojis(emoji_list.travel, title: 'travel'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.OBJECTS,
|
||||
await _getAvailableEmojis(emoji_list.objects, title: 'objects'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.SYMBOLS,
|
||||
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FLAGS,
|
||||
await _getAvailableEmojis(emoji_list.flags, title: 'flags'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// Get available emoji for given category title
|
||||
Future<List<Emoji>> _getAvailableEmojis(
|
||||
Map<String, String> map, {
|
||||
required String title,
|
||||
}) async {
|
||||
Map<String, String>? newMap;
|
||||
|
||||
// Get Emojis cached locally if available
|
||||
newMap = await _restoreFilteredEmojis(title);
|
||||
|
||||
if (newMap == null) {
|
||||
// Check if emoji is available on this platform
|
||||
newMap = await _getPlatformAvailableEmoji(map);
|
||||
// Save available Emojis to local storage for faster loading next time
|
||||
if (newMap != null) {
|
||||
await _cacheFilteredEmojis(title, newMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Map to Emoji Object
|
||||
return newMap!.entries
|
||||
.map<Emoji>((entry) => Emoji(entry.key, entry.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Check if emoji is available on current platform
|
||||
Future<Map<String, String>?> _getPlatformAvailableEmoji(
|
||||
Map<String, String> emoji,
|
||||
) async {
|
||||
if (Platform.isAndroid) {
|
||||
Map<String, String>? filtered = {};
|
||||
const delimiter = '|';
|
||||
try {
|
||||
final entries = emoji.values.join(delimiter);
|
||||
final keys = emoji.keys.join(delimiter);
|
||||
final result = (await platform.invokeMethod<String>(
|
||||
'checkAvailability',
|
||||
{'emojiKeys': keys, 'emojiEntries': entries},
|
||||
)) as String;
|
||||
final resultKeys = result.split(delimiter);
|
||||
for (var i = 0; i < resultKeys.length; i++) {
|
||||
filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
|
||||
}
|
||||
} on PlatformException catch (_) {
|
||||
filtered = null;
|
||||
}
|
||||
return filtered;
|
||||
} else {
|
||||
return emoji;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore locally cached emoji
|
||||
Future<Map<String, String>?> _restoreFilteredEmojis(String title) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final emojiJson = prefs.getString(title);
|
||||
if (emojiJson == null) {
|
||||
return null;
|
||||
}
|
||||
final emojis =
|
||||
Map<String, String>.from(jsonDecode(emojiJson) as Map<String, dynamic>);
|
||||
return emojis;
|
||||
}
|
||||
|
||||
// Stores filtered emoji locally for faster access next time
|
||||
Future<void> _cacheFilteredEmojis(
|
||||
String title,
|
||||
Map<String, String> emojis,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final emojiJson = jsonEncode(emojis);
|
||||
prefs.setString(title, emojiJson);
|
||||
}
|
||||
|
||||
// Returns list of recently used emoji from cache
|
||||
Future<List<RecentEmoji>> _getRecentEmojis() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final emojiJson = prefs.getString('recent');
|
||||
if (emojiJson == null) {
|
||||
return [];
|
||||
}
|
||||
final json = jsonDecode(emojiJson) as List<dynamic>;
|
||||
return json.map<RecentEmoji>(RecentEmoji.fromJson).toList();
|
||||
}
|
||||
|
||||
// Add an emoji to recently used list or increase its counter
|
||||
Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final recentEmojiIndex =
|
||||
recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji);
|
||||
if (recentEmojiIndex != -1) {
|
||||
// Already exist in recent list
|
||||
// Just update counter
|
||||
recentEmoji[recentEmojiIndex].counter++;
|
||||
} else {
|
||||
recentEmoji.add(RecentEmoji(emoji, 1));
|
||||
}
|
||||
// Sort by counter desc
|
||||
recentEmoji.sort((a, b) => b.counter - a.counter);
|
||||
// Limit entries to recentsLimit
|
||||
recentEmoji = recentEmoji.sublist(
|
||||
0,
|
||||
min(widget.config.recentsLimit, recentEmoji.length),
|
||||
);
|
||||
// save locally
|
||||
prefs.setString('recent', jsonEncode(recentEmoji));
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
onClose: () => keepEditorFocusNotifier.value -= 1,
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
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';
|
||||
|
||||
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();
|
||||
|
||||
Widget child = src.isEmpty
|
||||
? ImagePlaceholder(
|
||||
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);
|
||||
}
|
||||
}
|
||||
+2
-47
@@ -1,12 +1,4 @@
|
||||
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_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 +8,7 @@ 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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
handler: (editorState, menuService, context) async {
|
||||
return await editorState.insertImageNode('');
|
||||
},
|
||||
);
|
||||
+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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -55,7 +55,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
actions: SmartEditAction.values
|
||||
.map((action) => SmartEditActionWrapper(action))
|
||||
.toList(),
|
||||
onClosed: () => keepEditorFocusNotifier.value -= 1,
|
||||
buildChild: (controller) {
|
||||
keepEditorFocusNotifier.value += 1;
|
||||
return FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
tooltipText: isOpenAIEnabled
|
||||
|
||||
+1
-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';
|
||||
|
||||
+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}';
|
||||
}
|
||||
}
|
||||
+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
|
||||
|
||||
@@ -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.delayed(
|
||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
size: const Size(18, 18),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -82,6 +82,7 @@ class ReminderReferenceService {
|
||||
.toList();
|
||||
|
||||
if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {
|
||||
_setOptions();
|
||||
options = _allOptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -85,6 +86,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(),
|
||||
);
|
||||
|
||||
@@ -66,6 +66,7 @@ class InitAppWidgetTask extends LaunchTask {
|
||||
Locale('ru', 'RU'),
|
||||
Locale('sv'),
|
||||
Locale('tr', 'TR'),
|
||||
Locale('uk', 'UA'),
|
||||
Locale('ur'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class MockAuthService implements AuthService {
|
||||
final uuid = response.user!.id;
|
||||
final email = response.user!.email!;
|
||||
|
||||
final payload = OAuthPB(
|
||||
final payload = OauthSignInPB(
|
||||
authType: AuthTypePB.Supabase,
|
||||
map: {
|
||||
AuthServiceMapKeys.uuid: uuid,
|
||||
@@ -65,7 +65,7 @@ class MockAuthService implements AuthService {
|
||||
},
|
||||
);
|
||||
|
||||
return UserEventOAuth(payload).send().then((value) => value.swap());
|
||||
return UserEventOauthSignIn(payload).send().then((value) => value.swap());
|
||||
} on AuthException catch (e) {
|
||||
Log.error(e);
|
||||
return Left(AuthError.supabaseSignInError);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
class UserBackendService {
|
||||
@@ -26,6 +26,7 @@ class UserBackendService {
|
||||
String? email,
|
||||
String? iconUrl,
|
||||
String? openAIKey,
|
||||
String? stabilityAiKey,
|
||||
}) {
|
||||
final payload = UpdateUserProfilePayloadPB.create()..id = userId;
|
||||
|
||||
@@ -49,6 +50,10 @@ class UserBackendService {
|
||||
payload.openaiKey = openAIKey;
|
||||
}
|
||||
|
||||
if (stabilityAiKey != null) {
|
||||
payload.stabilityAiKey = stabilityAiKey;
|
||||
}
|
||||
|
||||
return UserEventUpdateUserProfile(payload).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
part 'settings_user_bloc.freezed.dart';
|
||||
|
||||
@@ -60,6 +60,16 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserStabilityAIKey: (stabilityAIKey) {
|
||||
_userService
|
||||
.updateUserProfile(stabilityAiKey: stabilityAIKey)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
|
||||
emit(state.copyWith(historicalUsers: historicalUsers));
|
||||
},
|
||||
@@ -119,6 +129,9 @@ class SettingsUserEvent with _$SettingsUserEvent {
|
||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
|
||||
_UpdateUserOpenaiKey;
|
||||
const factory SettingsUserEvent.updateUserStabilityAIKey(
|
||||
String stabilityAIKey,
|
||||
) = _UpdateUserStabilityAIKey;
|
||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
) = _DidReceiveUserProfile;
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -69,7 +70,7 @@ class SidebarTopMenu extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
);
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
richMessage: textSpan,
|
||||
child: FlowyIconButton(
|
||||
width: 28,
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
@@ -64,7 +65,7 @@ class SidebarUser extends StatelessWidget {
|
||||
|
||||
Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
|
||||
final userProfile = state.userProfile;
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
message: LocaleKeys.settings_menu_open.tr(),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -338,7 +339,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
|
||||
// + button
|
||||
Widget _buildViewAddButton(BuildContext context) {
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
|
||||
child: ViewAddButton(
|
||||
parentViewId: widget.view.id,
|
||||
@@ -378,7 +379,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
|
||||
// ··· more action button
|
||||
Widget _buildViewMoreActionButton(BuildContext context) {
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
|
||||
child: ViewMoreActionButton(
|
||||
view: widget.view,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -65,7 +66,7 @@ class FlowyNavigation extends StatelessWidget {
|
||||
if (state.isMenuCollapsed) {
|
||||
return RotationTransition(
|
||||
turns: const AlwaysStoppedAnimation(180 / 360),
|
||||
child: Tooltip(
|
||||
child: FlowyTooltip.delayed(
|
||||
richMessage: sidebarTooltipTextSpan(
|
||||
context,
|
||||
LocaleKeys.sideBar_openSidebar.tr(),
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -23,7 +24,7 @@ class NotificationButton extends StatelessWidget {
|
||||
return BlocProvider<ReminderBloc>.value(
|
||||
value: getIt<ReminderBloc>(),
|
||||
child: BlocBuilder<ReminderBloc, ReminderState>(
|
||||
builder: (context, state) => Tooltip(
|
||||
builder: (context, state) => FlowyTooltip.delayed(
|
||||
message: LocaleKeys.notificationHub_title.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
|
||||
+1
-11
@@ -113,17 +113,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
|
||||
Widget build(BuildContext context) {
|
||||
return EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
|
||||
config: const Config(
|
||||
columns: 7,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Colors.grey,
|
||||
iconColorSelected: Color(0xff333333),
|
||||
indicatorColor: Color(0xff333333),
|
||||
progressIndicatorColor: Color(0xff333333),
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
config: buildFlowyEmojiPickerConfig(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export 'emoji_menu_item.dart';
|
||||
export 'src/emji_picker_config.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
||||
export 'src/flowy_emoji_picker_config.dart';
|
||||
export 'src/models/emoji_model.dart';
|
||||
+68
-75
@@ -1,16 +1,19 @@
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.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 'config.dart';
|
||||
import 'emji_picker_config.dart';
|
||||
import 'emoji_picker.dart';
|
||||
import 'emoji_picker_builder.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
import 'models/category_models.dart';
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'models/emoji_model.dart';
|
||||
|
||||
class DefaultEmojiPickerView extends EmojiPickerBuilder {
|
||||
const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key})
|
||||
: super(config, state, key: key);
|
||||
const DefaultEmojiPickerView(
|
||||
EmojiPickerConfig config,
|
||||
EmojiViewState state, {
|
||||
Key? key,
|
||||
}) : super(config, state, key: key);
|
||||
|
||||
@override
|
||||
DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState();
|
||||
@@ -22,11 +25,12 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
TabController? _tabController;
|
||||
final TextEditingController _emojiController = TextEditingController();
|
||||
final FocusNode _emojiFocusNode = FocusNode();
|
||||
CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, <Emoji>[]);
|
||||
EmojiCategoryGroup searchEmojiList =
|
||||
EmojiCategoryGroup(EmojiCategory.SEARCH, <Emoji>[]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
var initCategory = widget.state.categoryEmoji.indexWhere(
|
||||
var initCategory = widget.state.emojiCategoryGroupList.indexWhere(
|
||||
(element) => element.category == widget.config.initCategory,
|
||||
);
|
||||
if (initCategory == -1) {
|
||||
@@ -34,7 +38,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
}
|
||||
_tabController = TabController(
|
||||
initialIndex: initCategory,
|
||||
length: widget.state.categoryEmoji.length,
|
||||
length: widget.state.emojiCategoryGroupList.length,
|
||||
vsync: this,
|
||||
);
|
||||
_pageController = PageController(initialPage: initCategory);
|
||||
@@ -49,7 +53,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
} else {
|
||||
searchEmojiList.emoji.clear();
|
||||
for (final element in widget.state.categoryEmoji) {
|
||||
for (final element in widget.state.emojiCategoryGroupList) {
|
||||
searchEmojiList.emoji.addAll(
|
||||
element.emoji.where((item) {
|
||||
return item.name.toLowerCase().contains(query);
|
||||
@@ -66,6 +70,8 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
void dispose() {
|
||||
_emojiController.dispose();
|
||||
_emojiFocusNode.dispose();
|
||||
_pageController?.dispose();
|
||||
_tabController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -100,56 +106,50 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
|
||||
final style = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
color: widget.config.bgColor,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
children: [
|
||||
const VSpace(4),
|
||||
// search bar
|
||||
SizedBox(
|
||||
height: 25.0,
|
||||
height: 32.0,
|
||||
child: TextField(
|
||||
controller: _emojiController,
|
||||
focusNode: _emojiFocusNode,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
cursorWidth: 1.0,
|
||||
cursorColor: Colors.black,
|
||||
style: style.textTheme.bodyMedium,
|
||||
cursorColor: style.textTheme.bodyMedium?.color,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
hintText: "Search emoji",
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
borderSide: const BorderSide(),
|
||||
gapPadding: 0.0,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
borderSide: const BorderSide(),
|
||||
gapPadding: 0.0,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
hoverColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
hintText: widget.config.searchHintText,
|
||||
hintStyle: widget.config.serachHintTextStyle,
|
||||
enabledBorder: widget.config.serachBarEnableBorder,
|
||||
focusedBorder: widget.config.serachBarFocusedBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
labelColor: widget.config.iconColorSelected,
|
||||
unselectedLabelColor: widget.config.iconColor,
|
||||
labelColor: widget.config.selectedCategoryIconColor,
|
||||
unselectedLabelColor: widget.config.categoryIconColor,
|
||||
controller: isEmojiSearching()
|
||||
? TabController(length: 1, vsync: this)
|
||||
: _tabController,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
indicatorColor: widget.config.indicatorColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
indicatorColor:
|
||||
widget.config.selectedCategoryIconBackgroundColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
indicator: BoxDecoration(
|
||||
border: Border.all(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: style.colorScheme.secondary,
|
||||
),
|
||||
onTap: (index) {
|
||||
_pageController!.animateToPage(
|
||||
@@ -159,8 +159,8 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
},
|
||||
tabs: isEmojiSearching()
|
||||
? [_buildCategory(Category.SEARCH, emojiSize)]
|
||||
: widget.state.categoryEmoji
|
||||
? [_buildCategory(EmojiCategory.SEARCH, emojiSize)]
|
||||
: widget.state.emojiCategoryGroupList
|
||||
.asMap()
|
||||
.entries
|
||||
.map<Widget>(
|
||||
@@ -179,20 +179,15 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
child: PageView.builder(
|
||||
itemCount: searchEmojiList.emoji.isNotEmpty
|
||||
? 1
|
||||
: widget.state.categoryEmoji.length,
|
||||
: widget.state.emojiCategoryGroupList.length,
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// onPageChanged: (index) {
|
||||
// _tabController!.animateTo(
|
||||
// index,
|
||||
// duration: widget.config.tabIndicatorAnimDuration,
|
||||
// );
|
||||
// },
|
||||
itemBuilder: (context, index) {
|
||||
final CategoryEmoji catEmoji = isEmojiSearching()
|
||||
? searchEmojiList
|
||||
: widget.state.categoryEmoji[index];
|
||||
return _buildPage(emojiSize, catEmoji);
|
||||
final EmojiCategoryGroup emojiCategoryGroup =
|
||||
isEmojiSearching()
|
||||
? searchEmojiList
|
||||
: widget.state.emojiCategoryGroupList[index];
|
||||
return _buildPage(emojiSize, emojiCategoryGroup);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -203,7 +198,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategory(Category category, double categorySize) {
|
||||
Widget _buildCategory(EmojiCategory category, double categorySize) {
|
||||
return Tab(
|
||||
height: categorySize,
|
||||
child: Icon(
|
||||
@@ -229,39 +224,39 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
|
||||
Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) {
|
||||
// Display notice if recent has no entries yet
|
||||
final scrollController = ScrollController();
|
||||
|
||||
if (categoryEmoji.category == Category.RECENT &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
if (emojiCategoryGroup.category == EmojiCategory.RECENT &&
|
||||
emojiCategoryGroup.emoji.isEmpty) {
|
||||
return _buildNoRecent();
|
||||
} else if (categoryEmoji.category == Category.SEARCH &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
return const Center(child: Text("No Emoji Found"));
|
||||
} else if (emojiCategoryGroup.category == EmojiCategory.SEARCH &&
|
||||
emojiCategoryGroup.emoji.isEmpty) {
|
||||
return Center(child: Text(widget.config.noEmojiFoundText));
|
||||
}
|
||||
// Build page normally
|
||||
return ScrollbarListStack(
|
||||
axis: Axis.vertical,
|
||||
controller: scrollController,
|
||||
barSize: 4.0,
|
||||
scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
handleColor: const Color(0xffDFE0E0),
|
||||
trackColor: const Color(0xffDFE0E0),
|
||||
scrollbarPadding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
handleColor: widget.config.scrollBarHandleColor,
|
||||
showTrack: true,
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: GridView.builder(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(0),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: widget.config.columns,
|
||||
crossAxisCount: widget.config.emojiNumberPerRow,
|
||||
mainAxisSpacing: widget.config.verticalSpacing,
|
||||
crossAxisSpacing: widget.config.horizontalSpacing,
|
||||
),
|
||||
itemCount: categoryEmoji.emoji.length,
|
||||
itemCount: emojiCategoryGroup.emoji.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = categoryEmoji.emoji[index];
|
||||
return _buildEmoji(emojiSize, categoryEmoji, item);
|
||||
final item = emojiCategoryGroup.emoji[index];
|
||||
return _buildEmoji(emojiSize, emojiCategoryGroup, item);
|
||||
},
|
||||
cacheExtent: 10,
|
||||
),
|
||||
@@ -271,21 +266,20 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
|
||||
Widget _buildEmoji(
|
||||
double emojiSize,
|
||||
CategoryEmoji categoryEmoji,
|
||||
EmojiCategoryGroup emojiCategoryGroup,
|
||||
Emoji emoji,
|
||||
) {
|
||||
return _buildButtonWidget(
|
||||
onPressed: () {
|
||||
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||
widget.state.onEmojiSelected(emojiCategoryGroup.category, emoji);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: FlowyHover(
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -294,10 +288,9 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
|
||||
Widget _buildNoRecent() {
|
||||
return Center(
|
||||
child: FlowyText.regular(
|
||||
child: Text(
|
||||
widget.config.noRecentsText,
|
||||
color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
|
||||
fontSize: widget.config.noRecentsStyle.fontSize,
|
||||
style: widget.config.noRecentsStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
part 'emji_picker_config.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class EmojiPickerConfig with _$EmojiPickerConfig {
|
||||
// private empty constructor is used to make method work in freezed
|
||||
// https://pub.dev/packages/freezed#adding-getters-and-methods-to-our-models
|
||||
const EmojiPickerConfig._();
|
||||
const factory EmojiPickerConfig({
|
||||
@Default(7) int emojiNumberPerRow,
|
||||
// The maximum size(width and height) of emoji
|
||||
// It also depaneds on the screen size and emojiNumberPerRow
|
||||
@Default(32) double emojiSizeMax,
|
||||
// Vertical spacing between emojis
|
||||
@Default(0) double verticalSpacing,
|
||||
// Horizontal spacing between emojis
|
||||
@Default(0) double horizontalSpacing,
|
||||
// The initial [EmojiCategory] that will be selected
|
||||
@Default(EmojiCategory.RECENT) EmojiCategory initCategory,
|
||||
// The background color of the Widget
|
||||
@Default(Color(0xFFEBEFF2)) Color? bgColor,
|
||||
// The color of the category icons
|
||||
@Default(Colors.grey) Color? categoryIconColor,
|
||||
// The color of the category icon when selected
|
||||
@Default(Colors.blue) Color? selectedCategoryIconColor,
|
||||
// The color of the category indicator
|
||||
@Default(Colors.blue) Color? selectedCategoryIconBackgroundColor,
|
||||
// The color of the loading indicator during initialization
|
||||
@Default(Colors.blue) Color? progressIndicatorColor,
|
||||
// The color of the backspace icon button
|
||||
@Default(Colors.blue) Color? backspaceColor,
|
||||
// Show extra tab with recently used emoji
|
||||
@Default(true) bool showRecentsTab,
|
||||
// Limit of recently used emoji that will be saved
|
||||
@Default(28) int recentsLimit,
|
||||
@Default('Search emoji') String searchHintText,
|
||||
TextStyle? serachHintTextStyle,
|
||||
InputBorder? serachBarEnableBorder,
|
||||
InputBorder? serachBarFocusedBorder,
|
||||
// The text to be displayed if no recent emojis to display
|
||||
@Default('No recent emoji') String noRecentsText,
|
||||
TextStyle? noRecentsStyle,
|
||||
// The text to be displayed if no emoji found
|
||||
@Default('No emoji found') String noEmojiFoundText,
|
||||
Color? scrollBarHandleColor,
|
||||
// Duration of tab indicator to animate to next category
|
||||
@Default(kTabScrollDuration) Duration tabIndicatorAnimDuration,
|
||||
// Determines the icon to display for each [EmojiCategory]
|
||||
@Default(EmojiCategoryIcons()) EmojiCategoryIcons emojiCategoryIcons,
|
||||
// Change between Material and Cupertino button style
|
||||
@Default(ButtonMode.MATERIAL) ButtonMode buttonMode,
|
||||
}) = _EmojiPickerConfig;
|
||||
|
||||
/// Get Emoji size based on properties and screen width
|
||||
double getEmojiSize(double width) {
|
||||
final maxSize = width / emojiNumberPerRow;
|
||||
return min(maxSize, emojiSizeMax);
|
||||
}
|
||||
|
||||
/// Returns the icon for the category
|
||||
IconData getIconForCategory(EmojiCategory category) {
|
||||
switch (category) {
|
||||
case EmojiCategory.RECENT:
|
||||
return emojiCategoryIcons.recentIcon;
|
||||
case EmojiCategory.SMILEYS:
|
||||
return emojiCategoryIcons.smileyIcon;
|
||||
case EmojiCategory.ANIMALS:
|
||||
return emojiCategoryIcons.animalIcon;
|
||||
case EmojiCategory.FOODS:
|
||||
return emojiCategoryIcons.foodIcon;
|
||||
case EmojiCategory.TRAVEL:
|
||||
return emojiCategoryIcons.travelIcon;
|
||||
case EmojiCategory.ACTIVITIES:
|
||||
return emojiCategoryIcons.activityIcon;
|
||||
case EmojiCategory.OBJECTS:
|
||||
return emojiCategoryIcons.objectIcon;
|
||||
case EmojiCategory.SYMBOLS:
|
||||
return emojiCategoryIcons.symbolIcon;
|
||||
case EmojiCategory.FLAGS:
|
||||
return emojiCategoryIcons.flagIcon;
|
||||
case EmojiCategory.SEARCH:
|
||||
return emojiCategoryIcons.searchIcon;
|
||||
default:
|
||||
throw Exception('Unsupported EmojiCategory');
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+52
-50
@@ -7,18 +7,16 @@ import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'models/category_models.dart';
|
||||
import 'config.dart';
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'emji_picker_config.dart';
|
||||
import 'default_emoji_picker_view.dart';
|
||||
import 'models/emoji_model.dart';
|
||||
import 'emoji_lists.dart' as emoji_list;
|
||||
import 'emoji_view_state.dart';
|
||||
import 'models/recent_emoji_model.dart';
|
||||
|
||||
/// All the possible categories that [Emoji] can be put into
|
||||
///
|
||||
/// All [Category] are shown in the category bar
|
||||
enum Category {
|
||||
/// The emoji category shown on the category tab
|
||||
enum EmojiCategory {
|
||||
/// Searched emojis
|
||||
SEARCH,
|
||||
|
||||
@@ -62,29 +60,32 @@ enum ButtonMode {
|
||||
/// Callback function for when emoji is selected
|
||||
///
|
||||
/// The function returns the selected [Emoji] as well
|
||||
/// as the [Category] from which it originated
|
||||
typedef OnEmojiSelected = void Function(Category category, Emoji emoji);
|
||||
/// as the [EmojiCategory] from which it originated
|
||||
typedef OnEmojiSelected = void Function(EmojiCategory category, Emoji emoji);
|
||||
|
||||
/// Callback function for backspace button
|
||||
typedef OnBackspacePressed = void Function();
|
||||
|
||||
/// Callback function for custom view
|
||||
typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state);
|
||||
typedef EmojiViewBuilder = Widget Function(
|
||||
EmojiPickerConfig config,
|
||||
EmojiViewState state,
|
||||
);
|
||||
|
||||
/// The Emoji Keyboard widget
|
||||
///
|
||||
/// This widget displays a grid of [Emoji] sorted by [Category]
|
||||
/// This widget displays a grid of [Emoji] sorted by [EmojiCategory]
|
||||
/// which the user can horizontally scroll through.
|
||||
///
|
||||
/// There is also a bottombar which displays all the possible [Category]
|
||||
/// and allow the user to quickly switch to that [Category]
|
||||
/// There is also a bottombar which displays all the possible [EmojiCategory]
|
||||
/// and allow the user to quickly switch to that [EmojiCategory]
|
||||
class EmojiPicker extends StatefulWidget {
|
||||
/// EmojiPicker for flutter
|
||||
const EmojiPicker({
|
||||
Key? key,
|
||||
required this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
this.config = const Config(),
|
||||
this.config = const EmojiPickerConfig(),
|
||||
this.customWidget,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -98,7 +99,7 @@ class EmojiPicker extends StatefulWidget {
|
||||
final OnBackspacePressed? onBackspacePressed;
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
final EmojiPickerConfig config;
|
||||
|
||||
@override
|
||||
EmojiPickerState createState() => EmojiPickerState();
|
||||
@@ -107,8 +108,8 @@ class EmojiPicker extends StatefulWidget {
|
||||
class EmojiPickerState extends State<EmojiPicker> {
|
||||
static const platform = MethodChannel('emoji_picker_flutter');
|
||||
|
||||
List<CategoryEmoji> categoryEmoji = List.empty(growable: true);
|
||||
List<RecentEmoji> recentEmoji = List.empty(growable: true);
|
||||
List<EmojiCategoryGroup> emojiCategoryGroupList = List.empty(growable: true);
|
||||
List<RecentEmoji> recentEmojiList = List.empty(growable: true);
|
||||
late Future<void> updateEmojiFuture;
|
||||
|
||||
// Prevent emojis to be reloaded with every build
|
||||
@@ -123,7 +124,7 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
@override
|
||||
void didUpdateWidget(covariant EmojiPicker oldWidget) {
|
||||
if (oldWidget.config != widget.config) {
|
||||
// Config changed - rebuild EmojiPickerView completely
|
||||
// EmojiPickerConfig changed - rebuild EmojiPickerView completely
|
||||
loaded = false;
|
||||
updateEmojiFuture = _updateEmojis();
|
||||
}
|
||||
@@ -147,12 +148,12 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (widget.config.showRecentsTab) {
|
||||
categoryEmoji[0].emoji =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
emojiCategoryGroupList[0].emoji =
|
||||
recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
}
|
||||
|
||||
final state = EmojiViewState(
|
||||
categoryEmoji,
|
||||
emojiCategoryGroupList,
|
||||
_getOnEmojiListener(),
|
||||
widget.onBackspacePressed,
|
||||
);
|
||||
@@ -168,7 +169,7 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
return (category, emoji) {
|
||||
if (widget.config.showRecentsTab) {
|
||||
_addEmojiToRecentlyUsed(emoji).then((value) {
|
||||
if (category != Category.RECENT && mounted) {
|
||||
if (category != EmojiCategory.RECENT && mounted) {
|
||||
setState(() {
|
||||
// rebuild to update recent emoji tab
|
||||
// when it is not current tab
|
||||
@@ -182,47 +183,48 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
|
||||
// Initialize emoji data
|
||||
Future<void> _updateEmojis() async {
|
||||
categoryEmoji.clear();
|
||||
emojiCategoryGroupList.clear();
|
||||
if (widget.config.showRecentsTab) {
|
||||
recentEmoji = await _getRecentEmojis();
|
||||
recentEmojiList = await _getRecentEmojis();
|
||||
final List<Emoji> recentEmojiMap =
|
||||
recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
|
||||
recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();
|
||||
emojiCategoryGroupList
|
||||
.add(EmojiCategoryGroup(EmojiCategory.RECENT, recentEmojiMap));
|
||||
}
|
||||
categoryEmoji.addAll([
|
||||
CategoryEmoji(
|
||||
Category.SMILEYS,
|
||||
emojiCategoryGroupList.addAll([
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.SMILEYS,
|
||||
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ANIMALS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.ANIMALS,
|
||||
await _getAvailableEmojis(emoji_list.animals, title: 'animals'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FOODS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.FOODS,
|
||||
await _getAvailableEmojis(emoji_list.foods, title: 'foods'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.ACTIVITIES,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.ACTIVITIES,
|
||||
await _getAvailableEmojis(
|
||||
emoji_list.activities,
|
||||
title: 'activities',
|
||||
),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.TRAVEL,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.TRAVEL,
|
||||
await _getAvailableEmojis(emoji_list.travel, title: 'travel'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.OBJECTS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.OBJECTS,
|
||||
await _getAvailableEmojis(emoji_list.objects, title: 'objects'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.SYMBOLS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.SYMBOLS,
|
||||
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),
|
||||
),
|
||||
CategoryEmoji(
|
||||
Category.FLAGS,
|
||||
EmojiCategoryGroup(
|
||||
EmojiCategory.FLAGS,
|
||||
await _getAvailableEmojis(emoji_list.flags, title: 'flags'),
|
||||
)
|
||||
]);
|
||||
@@ -316,23 +318,23 @@ class EmojiPickerState extends State<EmojiPicker> {
|
||||
// Add an emoji to recently used list or increase its counter
|
||||
Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final recentEmojiIndex =
|
||||
recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji);
|
||||
final recentEmojiIndex = recentEmojiList
|
||||
.indexWhere((element) => element.emoji.emoji == emoji.emoji);
|
||||
if (recentEmojiIndex != -1) {
|
||||
// Already exist in recent list
|
||||
// Just update counter
|
||||
recentEmoji[recentEmojiIndex].counter++;
|
||||
recentEmojiList[recentEmojiIndex].counter++;
|
||||
} else {
|
||||
recentEmoji.add(RecentEmoji(emoji, 1));
|
||||
recentEmojiList.add(RecentEmoji(emoji, 1));
|
||||
}
|
||||
// Sort by counter desc
|
||||
recentEmoji.sort((a, b) => b.counter - a.counter);
|
||||
recentEmojiList.sort((a, b) => b.counter - a.counter);
|
||||
// Limit entries to recentsLimit
|
||||
recentEmoji = recentEmoji.sublist(
|
||||
recentEmojiList = recentEmojiList.sublist(
|
||||
0,
|
||||
min(widget.config.recentsLimit, recentEmoji.length),
|
||||
min(widget.config.recentsLimit, recentEmojiList.length),
|
||||
);
|
||||
// save locally
|
||||
prefs.setString('recent', jsonEncode(recentEmoji));
|
||||
prefs.setString('recent', jsonEncode(recentEmojiList));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
import 'emji_picker_config.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
|
||||
/// Template class for custom implementation
|
||||
@@ -11,7 +11,7 @@ abstract class EmojiPickerBuilder extends StatefulWidget {
|
||||
: super(key: key);
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
final EmojiPickerConfig config;
|
||||
|
||||
/// State that holds current emoji data
|
||||
final EmojiViewState state;
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
import 'models/category_models.dart';
|
||||
import 'models/emoji_category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
/// State that holds current emoji data
|
||||
class EmojiViewState {
|
||||
/// Constructor
|
||||
EmojiViewState(
|
||||
this.categoryEmoji,
|
||||
this.emojiCategoryGroupList,
|
||||
this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
);
|
||||
|
||||
/// List of all category including their emoji
|
||||
final List<CategoryEmoji> categoryEmoji;
|
||||
/// List of all categories including their emojis
|
||||
final List<EmojiCategoryGroup> emojiCategoryGroupList;
|
||||
|
||||
/// Callback when pressed on emoji
|
||||
final OnEmojiSelected onEmojiSelected;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
EmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) {
|
||||
final style = Theme.of(context);
|
||||
return EmojiPickerConfig(
|
||||
bgColor: style.cardColor,
|
||||
categoryIconColor: style.iconTheme.color,
|
||||
selectedCategoryIconColor: style.colorScheme.onSurface,
|
||||
selectedCategoryIconBackgroundColor: style.colorScheme.primary,
|
||||
progressIndicatorColor: style.colorScheme.primary,
|
||||
backspaceColor: style.colorScheme.primary,
|
||||
searchHintText: LocaleKeys.emoji_search.tr(),
|
||||
serachHintTextStyle: style.textTheme.bodyMedium?.copyWith(
|
||||
color: style.hintColor,
|
||||
),
|
||||
serachBarEnableBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(color: style.dividerColor),
|
||||
),
|
||||
serachBarFocusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(
|
||||
color: style.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
noRecentsText: LocaleKeys.emoji_noRecent.tr(),
|
||||
noRecentsStyle: style.textTheme.bodyMedium,
|
||||
noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(),
|
||||
scrollBarHandleColor: style.colorScheme.onBackground,
|
||||
);
|
||||
}
|
||||
+21
-23
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'emoji_model.dart';
|
||||
import '../emoji_picker.dart';
|
||||
|
||||
/// Container for Category and their emoji
|
||||
class CategoryEmoji {
|
||||
/// Constructor
|
||||
CategoryEmoji(this.category, this.emoji);
|
||||
/// EmojiCategory with its emojis
|
||||
class EmojiCategoryGroup {
|
||||
EmojiCategoryGroup(this.category, this.emoji);
|
||||
|
||||
/// Category instance
|
||||
final Category category;
|
||||
final EmojiCategory category;
|
||||
|
||||
/// List of emoji of this category
|
||||
List<Emoji> emoji;
|
||||
@@ -20,10 +18,10 @@ class CategoryEmoji {
|
||||
}
|
||||
}
|
||||
|
||||
/// Class that defines the icon representing a [Category]
|
||||
class CategoryIcon {
|
||||
/// Class that defines the icon representing a [EmojiCategory]
|
||||
class EmojiCategoryIcon {
|
||||
/// Icon of Category
|
||||
const CategoryIcon({
|
||||
const EmojiCategoryIcon({
|
||||
required this.icon,
|
||||
this.color = const Color(0xffd3d3d3),
|
||||
this.selectedColor = const Color(0xffb2b2b2),
|
||||
@@ -39,14 +37,14 @@ class CategoryIcon {
|
||||
final Color selectedColor;
|
||||
}
|
||||
|
||||
/// Class used to define all the [CategoryIcon] shown for each [Category]
|
||||
/// Class used to define all the [EmojiCategoryIcon] shown for each [EmojiCategory]
|
||||
///
|
||||
/// This allows the keyboard to be personalized by changing icons shown.
|
||||
/// If a [CategoryIcon] is set as null or not defined during initialization,
|
||||
/// If a [EmojiCategoryIcon] is set as null or not defined during initialization,
|
||||
/// the default icons will be used instead
|
||||
class CategoryIcons {
|
||||
class EmojiCategoryIcons {
|
||||
/// Constructor
|
||||
const CategoryIcons({
|
||||
const EmojiCategoryIcons({
|
||||
this.recentIcon = Icons.access_time,
|
||||
this.smileyIcon = Icons.tag_faces,
|
||||
this.animalIcon = Icons.pets,
|
||||
@@ -59,33 +57,33 @@ class CategoryIcons {
|
||||
this.searchIcon = Icons.search,
|
||||
});
|
||||
|
||||
/// Icon for [Category.RECENT]
|
||||
/// Icon for [EmojiCategory.RECENT]
|
||||
final IconData recentIcon;
|
||||
|
||||
/// Icon for [Category.SMILEYS]
|
||||
/// Icon for [EmojiCategory.SMILEYS]
|
||||
final IconData smileyIcon;
|
||||
|
||||
/// Icon for [Category.ANIMALS]
|
||||
/// Icon for [EmojiCategory.ANIMALS]
|
||||
final IconData animalIcon;
|
||||
|
||||
/// Icon for [Category.FOODS]
|
||||
/// Icon for [EmojiCategory.FOODS]
|
||||
final IconData foodIcon;
|
||||
|
||||
/// Icon for [Category.ACTIVITIES]
|
||||
/// Icon for [EmojiCategory.ACTIVITIES]
|
||||
final IconData activityIcon;
|
||||
|
||||
/// Icon for [Category.TRAVEL]
|
||||
/// Icon for [EmojiCategory.TRAVEL]
|
||||
final IconData travelIcon;
|
||||
|
||||
/// Icon for [Category.OBJECTS]
|
||||
/// Icon for [EmojiCategory.OBJECTS]
|
||||
final IconData objectIcon;
|
||||
|
||||
/// Icon for [Category.SYMBOLS]
|
||||
/// Icon for [EmojiCategory.SYMBOLS]
|
||||
final IconData symbolIcon;
|
||||
|
||||
/// Icon for [Category.FLAGS]
|
||||
/// Icon for [EmojiCategory.FLAGS]
|
||||
final IconData flagIcon;
|
||||
|
||||
/// Icon for [Category.SEARCH]
|
||||
/// Icon for [EmojiCategory.SEARCH]
|
||||
final IconData searchIcon;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+21
-34
@@ -16,42 +16,29 @@ class BrightnessSetting extends StatelessWidget {
|
||||
const BrightnessSetting({required this.currentThemeMode, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
richMessage: themeModeTooltipTextSpan(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||
Widget build(BuildContext context) {
|
||||
return ThemeSettingEntryTemplateWidget(
|
||||
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||
hint: hintText,
|
||||
onResetRequested: context.read<AppearanceSettingsCubit>().resetThemeMode,
|
||||
trailing: [
|
||||
ThemeValueDropDown(
|
||||
currentValue: _themeModeLabelText(currentThemeMode),
|
||||
popupBuilder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_themeModeItemButton(context, ThemeMode.light),
|
||||
_themeModeItemButton(context, ThemeMode.dark),
|
||||
_themeModeItemButton(context, ThemeMode.system),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: ThemeSettingEntryTemplateWidget(
|
||||
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||
onResetRequested:
|
||||
context.read<AppearanceSettingsCubit>().resetThemeMode,
|
||||
trailing: [
|
||||
ThemeValueDropDown(
|
||||
currentValue: _themeModeLabelText(currentThemeMode),
|
||||
popupBuilder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_themeModeItemButton(context, ThemeMode.light),
|
||||
_themeModeItemButton(context, ThemeMode.dark),
|
||||
_themeModeItemButton(context, ThemeMode.system),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan themeModeTooltipTextSpan(BuildContext context, String hintText) =>
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "$hintText\n",
|
||||
),
|
||||
TextSpan(
|
||||
text: Platform.isMacOS ? "⌘+Shift+L" : "Ctrl+Shift+L",
|
||||
),
|
||||
],
|
||||
);
|
||||
String get hintText =>
|
||||
'${LocaleKeys.settings_files_change.tr()} ${LocaleKeys.settings_appearance_themeMode_label.tr()} : ${Platform.isMacOS ? '⌘+Shift+L' : 'Ctrl+Shift+L'}';
|
||||
|
||||
Widget _themeModeItemButton(
|
||||
BuildContext context,
|
||||
|
||||
+15
-5
@@ -25,17 +25,27 @@ class SettingsAppearanceView extends StatelessWidget {
|
||||
currentTheme: state.appTheme.themeName,
|
||||
bloc: context.read<DynamicPluginBloc>(),
|
||||
),
|
||||
BrightnessSetting(currentThemeMode: state.themeMode),
|
||||
BrightnessSetting(
|
||||
currentThemeMode: state.themeMode,
|
||||
),
|
||||
const Divider(),
|
||||
ThemeFontFamilySetting(currentFontFamily: state.font),
|
||||
ThemeFontFamilySetting(
|
||||
currentFontFamily: state.font,
|
||||
),
|
||||
const Divider(),
|
||||
LayoutDirectionSetting(
|
||||
currentLayoutDirection: state.layoutDirection,
|
||||
),
|
||||
TextDirectionSetting(currentTextDirection: state.textDirection),
|
||||
TextDirectionSetting(
|
||||
currentTextDirection: state.textDirection,
|
||||
),
|
||||
const Divider(),
|
||||
DateFormatSetting(currentFormat: state.dateFormat),
|
||||
TimeFormatSetting(currentFormat: state.timeFormat),
|
||||
DateFormatSetting(
|
||||
currentFormat: state.dateFormat,
|
||||
),
|
||||
TimeFormatSetting(
|
||||
currentFormat: state.timeFormat,
|
||||
),
|
||||
const Divider(),
|
||||
CreateFileSettings(),
|
||||
],
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -163,7 +164,7 @@ class _ChangeStoragePathButton extends StatefulWidget {
|
||||
class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
message: LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
||||
child: SecondaryTextButton(
|
||||
LocaleKeys.settings_files_change.tr(),
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class SettingsLanguageView extends StatelessWidget {
|
||||
|
||||
class LanguageSelector extends StatelessWidget {
|
||||
final Locale currentLocale;
|
||||
|
||||
|
||||
const LanguageSelector({
|
||||
super.key,
|
||||
required this.currentLocale,
|
||||
|
||||
+8
-4
@@ -19,6 +19,9 @@ class SettingsMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool showSyncSetting = isCloudEnabled &&
|
||||
context.read<SettingsDialogBloc>().state.userProfile.authType !=
|
||||
AuthTypePB.Local;
|
||||
return Column(
|
||||
children: [
|
||||
SettingsMenuElement(
|
||||
@@ -58,11 +61,12 @@ class SettingsMenu extends StatelessWidget {
|
||||
icon: Icons.account_box_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
|
||||
if (showSyncSetting)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
// Only show supabase setting if supabase is enabled and the current auth type is not local
|
||||
if (isCloudEnabled &&
|
||||
context.read<SettingsDialogBloc>().state.userProfile.authType !=
|
||||
AuthTypePB.Local)
|
||||
if (showSyncSetting)
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.syncSetting,
|
||||
selectedPage: currentPage,
|
||||
|
||||
+44
-18
@@ -9,7 +9,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/flowy_tooltip.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -17,6 +16,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -61,6 +61,8 @@ class SettingsUserView extends StatelessWidget {
|
||||
const VSpace(12),
|
||||
_renderCurrentOpenaiKey(context),
|
||||
const VSpace(12),
|
||||
_renderCurrentStabilityAIKey(context),
|
||||
const VSpace(12),
|
||||
_renderLoginOrLogoutButton(context, state),
|
||||
const VSpace(12),
|
||||
],
|
||||
@@ -207,9 +209,29 @@ class SettingsUserView extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _renderCurrentOpenaiKey(BuildContext context) {
|
||||
final String openAIKey =
|
||||
final String accessKey =
|
||||
context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
|
||||
return _OpenaiKeyInput(openAIKey);
|
||||
return _AIAccessKeyInput(
|
||||
accessKey: accessKey,
|
||||
title: 'OpenAI Key',
|
||||
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
|
||||
callback: (key) => context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserOpenAIKey(key)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderCurrentStabilityAIKey(BuildContext context) {
|
||||
final String accessKey =
|
||||
context.read<SettingsUserViewBloc>().state.userProfile.stabilityAiKey;
|
||||
return _AIAccessKeyInput(
|
||||
accessKey: accessKey,
|
||||
title: 'Stability AI Key',
|
||||
hintText: LocaleKeys.settings_user_pleaseInputYourStabilityAIKey.tr(),
|
||||
callback: (key) => context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserStabilityAIKey(key)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _avatarOverlay({
|
||||
@@ -217,7 +239,7 @@ class SettingsUserView extends StatelessWidget {
|
||||
required bool hasIcon,
|
||||
required Widget child,
|
||||
}) =>
|
||||
FlowyTooltip.delayedTooltip(
|
||||
FlowyTooltip.delayed(
|
||||
message: LocaleKeys.settings_user_tooltipSelectIcon.tr(),
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -379,18 +401,24 @@ class UserEmailInputState extends State<UserEmailInput> {
|
||||
}
|
||||
}
|
||||
|
||||
class _OpenaiKeyInput extends StatefulWidget {
|
||||
final String openAIKey;
|
||||
const _OpenaiKeyInput(
|
||||
this.openAIKey, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
class _AIAccessKeyInput extends StatefulWidget {
|
||||
const _AIAccessKeyInput({
|
||||
required this.accessKey,
|
||||
required this.title,
|
||||
required this.hintText,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
final String accessKey;
|
||||
final String title;
|
||||
final String hintText;
|
||||
final void Function(String key) callback;
|
||||
|
||||
@override
|
||||
State<_OpenaiKeyInput> createState() => _OpenaiKeyInputState();
|
||||
State<_AIAccessKeyInput> createState() => _AIAccessKeyInputState();
|
||||
}
|
||||
|
||||
class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
class _AIAccessKeyInputState extends State<_AIAccessKeyInput> {
|
||||
bool visible = false;
|
||||
final textEditingController = TextEditingController();
|
||||
final debounce = Debounce();
|
||||
@@ -399,7 +427,7 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
textEditingController.text = widget.openAIKey;
|
||||
textEditingController.text = widget.accessKey;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -415,12 +443,12 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
labelText: 'OpenAI Key',
|
||||
labelText: widget.title,
|
||||
labelStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
|
||||
hintText: widget.hintText,
|
||||
suffixIcon: FlowyIconButton(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -437,9 +465,7 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
),
|
||||
onChanged: (value) {
|
||||
debounce.call(() {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserOpenAIKey(value));
|
||||
widget.callback(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -105,7 +106,7 @@ class EnableEncrypt extends StatelessWidget {
|
||||
const VSpace(6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Tooltip(
|
||||
child: FlowyTooltip.delayed(
|
||||
message: LocaleKeys.settings_menu_clickToCopySecret.tr(),
|
||||
child: FlowyButton(
|
||||
disable: !(state.config.enableEncrypt),
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export 'src/config.dart';
|
||||
export 'src/models/emoji_model.dart';
|
||||
export 'src/emoji_picker.dart';
|
||||
export 'src/emoji_picker_builder.dart';
|
||||
-169
@@ -1,169 +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.selectedHoverColor = Colors.grey,
|
||||
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 background color of the selected category
|
||||
final Color selectedHoverColor;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
-321
@@ -1,321 +0,0 @@
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'models/category_models.dart';
|
||||
import 'config.dart';
|
||||
import 'models/emoji_model.dart';
|
||||
import 'emoji_picker.dart';
|
||||
import 'emoji_picker_builder.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
|
||||
class DefaultEmojiPickerView extends EmojiPickerBuilder {
|
||||
const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key})
|
||||
: super(config, state, key: key);
|
||||
|
||||
@override
|
||||
DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState();
|
||||
}
|
||||
|
||||
class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
with TickerProviderStateMixin {
|
||||
PageController? _pageController;
|
||||
TabController? _tabController;
|
||||
final TextEditingController _emojiController = TextEditingController();
|
||||
final FocusNode _emojiFocusNode = FocusNode();
|
||||
final CategoryEmoji _categoryEmoji =
|
||||
CategoryEmoji(Category.SEARCH, List.empty(growable: true));
|
||||
CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, <Emoji>[]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
var initCategory = widget.state.categoryEmoji.indexWhere(
|
||||
(element) => element.category == widget.config.initCategory,
|
||||
);
|
||||
if (initCategory == -1) {
|
||||
initCategory = 0;
|
||||
}
|
||||
_tabController = TabController(
|
||||
initialIndex: initCategory,
|
||||
length: widget.state.categoryEmoji.length,
|
||||
vsync: this,
|
||||
);
|
||||
_pageController = PageController(initialPage: initCategory);
|
||||
_emojiFocusNode.requestFocus();
|
||||
|
||||
_emojiController.addListener(() {
|
||||
final String query = _emojiController.text.toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
searchEmojiList.emoji.clear();
|
||||
_pageController!.jumpToPage(
|
||||
_tabController!.index,
|
||||
);
|
||||
} else {
|
||||
searchEmojiList.emoji.clear();
|
||||
for (final element in widget.state.categoryEmoji) {
|
||||
searchEmojiList.emoji.addAll(
|
||||
element.emoji.where((item) {
|
||||
return item.name.toLowerCase().contains(query);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emojiController.dispose();
|
||||
_emojiFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildBackspaceButton() {
|
||||
if (widget.state.onBackspacePressed != null) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
icon: Icon(
|
||||
Icons.backspace,
|
||||
color: widget.config.backspaceColor,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.state.onBackspacePressed!();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
bool isEmojiSearching() {
|
||||
final bool result =
|
||||
searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
|
||||
|
||||
return Container(
|
||||
color: widget.config.bgColor,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: _emojiController,
|
||||
focusNode: _emojiFocusNode,
|
||||
autofocus: true,
|
||||
cursorWidth: 1.0,
|
||||
cursorColor: Theme.of(context).colorScheme.tertiary,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: FontSizes.s16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
hintText: "Search emoji",
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
labelColor: widget.config.iconColorSelected,
|
||||
unselectedLabelColor: widget.config.iconColor,
|
||||
controller: isEmojiSearching()
|
||||
? TabController(length: 1, vsync: this)
|
||||
: _tabController,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
indicatorColor: widget.config.indicatorColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: widget.config.selectedHoverColor,
|
||||
),
|
||||
onTap: (index) {
|
||||
_pageController!.animateToPage(
|
||||
index,
|
||||
duration: widget.config.tabIndicatorAnimDuration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
},
|
||||
tabs: isEmojiSearching()
|
||||
? [_buildCategory(Category.SEARCH, emojiSize)]
|
||||
: widget.state.categoryEmoji
|
||||
.asMap()
|
||||
.entries
|
||||
.map<Widget>(
|
||||
(item) => _buildCategory(
|
||||
item.value.category,
|
||||
emojiSize,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
_buildBackspaceButton(),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: PageView.builder(
|
||||
itemCount: searchEmojiList.emoji.isNotEmpty
|
||||
? 1
|
||||
: widget.state.categoryEmoji.length,
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// onPageChanged: (index) {
|
||||
// _tabController!.animateTo(
|
||||
// index,
|
||||
// duration: widget.config.tabIndicatorAnimDuration,
|
||||
// );
|
||||
// },
|
||||
itemBuilder: (context, index) {
|
||||
final CategoryEmoji catEmoji = isEmojiSearching()
|
||||
? searchEmojiList
|
||||
: widget.state.categoryEmoji[index];
|
||||
return _buildPage(emojiSize, catEmoji);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategory(Category category, double categorySize) {
|
||||
return Tab(
|
||||
height: categorySize,
|
||||
child: Icon(
|
||||
widget.config.getIconForCategory(category),
|
||||
size: categorySize / 1.3,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtonWidget({
|
||||
required VoidCallback onPressed,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (widget.config.buttonMode == ButtonMode.MATERIAL) {
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
|
||||
// Display notice if recent has no entries yet
|
||||
final scrollController = ScrollController();
|
||||
|
||||
if (categoryEmoji.category == Category.RECENT &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
return _buildNoRecent();
|
||||
} else if (categoryEmoji.category == Category.SEARCH &&
|
||||
categoryEmoji.emoji.isEmpty) {
|
||||
return const Center(child: Text("No Emoji Found"));
|
||||
}
|
||||
// Build page normally
|
||||
return ScrollbarListStack(
|
||||
axis: Axis.vertical,
|
||||
controller: scrollController,
|
||||
barSize: 4.0,
|
||||
scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
handleColor: const Color(0xffDFE0E0),
|
||||
trackColor: const Color(0xffDFE0E0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: GridView.count(
|
||||
scrollDirection: Axis.vertical,
|
||||
physics: const ScrollPhysics(),
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
// primary: true,
|
||||
padding: const EdgeInsets.all(0),
|
||||
crossAxisCount: widget.config.columns,
|
||||
mainAxisSpacing: widget.config.verticalSpacing,
|
||||
crossAxisSpacing: widget.config.horizontalSpacing,
|
||||
children: _categoryEmoji.emoji.isNotEmpty
|
||||
? _categoryEmoji.emoji
|
||||
.map<Widget>((e) => _buildEmoji(emojiSize, categoryEmoji, e))
|
||||
.toList()
|
||||
: categoryEmoji.emoji
|
||||
.map<Widget>(
|
||||
(item) => _buildEmoji(emojiSize, categoryEmoji, item),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmoji(
|
||||
double emojiSize,
|
||||
CategoryEmoji categoryEmoji,
|
||||
Emoji emoji,
|
||||
) {
|
||||
return _buildButtonWidget(
|
||||
onPressed: () {
|
||||
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
style: TextStyle(
|
||||
fontSize: emojiSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoRecent() {
|
||||
return Center(
|
||||
child: Text(
|
||||
widget.config.noRecentsText,
|
||||
style: widget.config.noRecentsStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-3223
File diff suppressed because it is too large.
Load diff
-18
@@ -1,18 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'config.dart';
|
||||
import 'emoji_view_state.dart';
|
||||
|
||||
/// Template class for custom implementation
|
||||
/// Inherit this class to create your own EmojiPicker
|
||||
abstract class EmojiPickerBuilder extends StatefulWidget {
|
||||
/// Constructor
|
||||
const EmojiPickerBuilder(this.config, this.state, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
/// Config for customizations
|
||||
final Config config;
|
||||
|
||||
/// State that holds current emoji data
|
||||
final EmojiViewState state;
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import 'models/category_models.dart';
|
||||
import 'emoji_picker.dart';
|
||||
|
||||
/// State that holds current emoji data
|
||||
class EmojiViewState {
|
||||
/// Constructor
|
||||
EmojiViewState(
|
||||
this.categoryEmoji,
|
||||
this.onEmojiSelected,
|
||||
this.onBackspacePressed,
|
||||
);
|
||||
|
||||
/// List of all category including their emoji
|
||||
final List<CategoryEmoji> categoryEmoji;
|
||||
|
||||
/// Callback when pressed on emoji
|
||||
final OnEmojiSelected onEmojiSelected;
|
||||
|
||||
/// Callback when pressed on backspace
|
||||
final OnBackspacePressed? onBackspacePressed;
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'emoji_model.dart';
|
||||
import '../emoji_picker.dart';
|
||||
|
||||
/// Container for Category and their emoji
|
||||
class CategoryEmoji {
|
||||
/// Constructor
|
||||
CategoryEmoji(this.category, this.emoji);
|
||||
|
||||
/// Category instance
|
||||
final Category category;
|
||||
|
||||
/// List of emoji of this category
|
||||
List<Emoji> emoji;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Name: $category, Emoji: $emoji';
|
||||
}
|
||||
}
|
||||
|
||||
/// Class that defines the icon representing a [Category]
|
||||
class CategoryIcon {
|
||||
/// Icon of Category
|
||||
const CategoryIcon({
|
||||
required this.icon,
|
||||
this.color = const Color(0xffd3d3d3),
|
||||
this.selectedColor = const Color(0xffb2b2b2),
|
||||
});
|
||||
|
||||
/// The icon to represent the category
|
||||
final IconData icon;
|
||||
|
||||
/// The default color of the icon
|
||||
final Color color;
|
||||
|
||||
/// The color of the icon once the category is selected
|
||||
final Color selectedColor;
|
||||
}
|
||||
|
||||
/// Class used to define all the [CategoryIcon] shown for each [Category]
|
||||
///
|
||||
/// This allows the keyboard to be personalized by changing icons shown.
|
||||
/// If a [CategoryIcon] is set as null or not defined during initialization,
|
||||
/// the default icons will be used instead
|
||||
class CategoryIcons {
|
||||
/// Constructor
|
||||
const CategoryIcons({
|
||||
this.recentIcon = Icons.access_time,
|
||||
this.smileyIcon = Icons.tag_faces,
|
||||
this.animalIcon = Icons.pets,
|
||||
this.foodIcon = Icons.fastfood,
|
||||
this.activityIcon = Icons.directions_run,
|
||||
this.travelIcon = Icons.location_city,
|
||||
this.objectIcon = Icons.lightbulb_outline,
|
||||
this.symbolIcon = Icons.emoji_symbols,
|
||||
this.flagIcon = Icons.flag,
|
||||
this.searchIcon = Icons.search,
|
||||
});
|
||||
|
||||
/// Icon for [Category.RECENT]
|
||||
final IconData recentIcon;
|
||||
|
||||
/// Icon for [Category.SMILEYS]
|
||||
final IconData smileyIcon;
|
||||
|
||||
/// Icon for [Category.ANIMALS]
|
||||
final IconData animalIcon;
|
||||
|
||||
/// Icon for [Category.FOODS]
|
||||
final IconData foodIcon;
|
||||
|
||||
/// Icon for [Category.ACTIVITIES]
|
||||
final IconData activityIcon;
|
||||
|
||||
/// Icon for [Category.TRAVEL]
|
||||
final IconData travelIcon;
|
||||
|
||||
/// Icon for [Category.OBJECTS]
|
||||
final IconData objectIcon;
|
||||
|
||||
/// Icon for [Category.SYMBOLS]
|
||||
final IconData symbolIcon;
|
||||
|
||||
/// Icon for [Category.FLAGS]
|
||||
final IconData flagIcon;
|
||||
|
||||
/// Icon for [Category.SEARCH]
|
||||
final IconData searchIcon;
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/// A class to store data for each individual emoji
|
||||
class Emoji {
|
||||
/// Emoji constructor
|
||||
const Emoji(this.name, this.emoji);
|
||||
|
||||
/// The name or description for this emoji
|
||||
final String name;
|
||||
|
||||
/// The unicode string for this emoji
|
||||
///
|
||||
/// This is the string that should be displayed to view the emoji
|
||||
final String emoji;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
// return 'Name: $name, Emoji: $emoji';
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Parse Emoji from json
|
||||
static Emoji fromJson(Map<String, dynamic> json) {
|
||||
return Emoji(json['name'] as String, json['emoji'] as String);
|
||||
}
|
||||
|
||||
/// Encode Emoji to json
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'emoji': emoji,
|
||||
};
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import 'emoji_model.dart';
|
||||
|
||||
/// Class that holds an recent emoji
|
||||
/// Recent Emoji has an instance of the emoji
|
||||
/// And a counter, which counts how often this emoji
|
||||
/// has been used before
|
||||
class RecentEmoji {
|
||||
/// Constructor
|
||||
RecentEmoji(this.emoji, this.counter);
|
||||
|
||||
/// Emoji instance
|
||||
final Emoji emoji;
|
||||
|
||||
/// Counter how often emoji has been used before
|
||||
int counter = 0;
|
||||
|
||||
/// Parse RecentEmoji from json
|
||||
static RecentEmoji fromJson(dynamic json) {
|
||||
return RecentEmoji(
|
||||
Emoji.fromJson(json['emoji'] as Map<String, dynamic>),
|
||||
json['counter'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// Encode RecentEmoji to json
|
||||
Map<String, dynamic> toJson() => {
|
||||
'emoji': emoji,
|
||||
'counter': counter,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
// Run `dart run build_runner build` to generate the json serialization If the
|
||||
// file `env_serde.i.dart` is existed, delete it first.
|
||||
// file `env_serde.g.dart` is existed, delete it first.
|
||||
//
|
||||
// the file `env_serde.g.dart` will be generated in the same directory. Rename
|
||||
// the file to `env_serde.i.dart` because the file is ignored by default.
|
||||
// the file `env_serde.g.dart` will be generated in the same directory.
|
||||
part 'env_serde.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -45,11 +44,13 @@ class SupabaseConfiguration {
|
||||
@JsonSerializable()
|
||||
class AppFlowyCloudConfiguration {
|
||||
final String base_url;
|
||||
final String base_ws_url;
|
||||
final String ws_base_url;
|
||||
final String gotrue_url;
|
||||
|
||||
AppFlowyCloudConfiguration({
|
||||
required this.base_url,
|
||||
required this.base_ws_url,
|
||||
required this.ws_base_url,
|
||||
required this.gotrue_url,
|
||||
});
|
||||
|
||||
factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'package:appflowy_popover/src/layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'mask.dart';
|
||||
import 'mutex.dart';
|
||||
|
||||
class PopoverController {
|
||||
PopoverState? _state;
|
||||
|
||||
close() {
|
||||
void close() {
|
||||
_state?.close();
|
||||
}
|
||||
|
||||
show() {
|
||||
void show() {
|
||||
_state?.showOverlay();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +48,11 @@ enum PopoverDirection {
|
||||
custom,
|
||||
}
|
||||
|
||||
enum PopoverClickHandler {
|
||||
listener,
|
||||
gestureDetector,
|
||||
}
|
||||
|
||||
class Popover extends StatefulWidget {
|
||||
final PopoverController? controller;
|
||||
|
||||
@@ -77,11 +83,18 @@ class Popover extends StatefulWidget {
|
||||
|
||||
final bool asBarrier;
|
||||
|
||||
/// The widget that will be used to trigger the popover.
|
||||
///
|
||||
/// Why do we need this?
|
||||
/// Because if the parent widget of the popover is GestureDetector,
|
||||
/// the conflict won't be resolve by using Listener, we want these two gestures exclusive.
|
||||
final PopoverClickHandler clickHandler;
|
||||
|
||||
/// The content area of the popover.
|
||||
final Widget child;
|
||||
|
||||
const Popover({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.popupBuilder,
|
||||
this.controller,
|
||||
@@ -96,7 +109,8 @@ class Popover extends StatefulWidget {
|
||||
this.onClose,
|
||||
this.canClose,
|
||||
this.asBarrier = false,
|
||||
}) : super(key: key);
|
||||
this.clickHandler = PopoverClickHandler.listener,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Popover> createState() => PopoverState();
|
||||
@@ -202,9 +216,9 @@ class PopoverState extends State<Popover> {
|
||||
showOverlay();
|
||||
}
|
||||
},
|
||||
child: Listener(
|
||||
child: widget.child,
|
||||
onPointerDown: (_) {
|
||||
child: _buildClickHandler(
|
||||
widget.child,
|
||||
() {
|
||||
if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
|
||||
showOverlay();
|
||||
}
|
||||
@@ -212,6 +226,21 @@ class PopoverState extends State<Popover> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClickHandler(Widget child, VoidCallback handler) {
|
||||
switch (widget.clickHandler) {
|
||||
case PopoverClickHandler.listener:
|
||||
return Listener(
|
||||
onPointerDown: (_) => handler(),
|
||||
child: child,
|
||||
);
|
||||
case PopoverClickHandler.gestureDetector:
|
||||
return GestureDetector(
|
||||
onTap: handler,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverContainer extends StatefulWidget {
|
||||
|
||||
@@ -57,7 +57,7 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
main1: _lightDandelionYellow,
|
||||
// cursor color
|
||||
main2: _lightDandelionYellow,
|
||||
shadow: _black,
|
||||
shadow: const Color.fromRGBO(0, 0, 0, 0.15),
|
||||
sidebarBg: _lightDandelionGreen,
|
||||
divider: _lightShader6,
|
||||
topbarBg: _white,
|
||||
@@ -111,7 +111,7 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
tint9: const Color(0x4d0029FF),
|
||||
main1: _darkMain1,
|
||||
main2: _darkMain1,
|
||||
shadow: _black,
|
||||
shadow: const Color(0xff0F131C),
|
||||
sidebarBg: const Color(0xff232B38),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
|
||||
@@ -107,7 +107,7 @@ class LavenderColorScheme extends FlowyColorScheme {
|
||||
tint9: const Color(0x4d0029FF),
|
||||
main1: _darkMain1,
|
||||
main2: _darkMain1,
|
||||
shadow: _black,
|
||||
shadow: const Color(0xff0F131C),
|
||||
sidebarBg: const Color(0xff2D223B),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
|
||||
@@ -57,6 +57,8 @@ String languageFromLocale(Locale locale) {
|
||||
return "Türkçe";
|
||||
case "fa":
|
||||
return "فارسی";
|
||||
case "uk":
|
||||
return "українська";
|
||||
case "ur":
|
||||
return "اردو";
|
||||
|
||||
|
||||
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user